gRPC Context and Cancellation

Every gRPC call carries a context. The context holds the deadline, the cancellation signal, and the metadata for that call. Handlers that respect the context stop wasted work the moment a caller loses interest.

What a Context Holds

ItemPurpose
DeadlineThe time after which the call must stop
Cancellation signalA flag that tells running code to quit
MetadataHeaders such as tokens and request IDs
Peer informationDetails about the caller

The Food Order Analogy

You order a pizza through an app and cancel two minutes later. The app tells the restaurant. The restaurant tells the kitchen. The kitchen stops kneading the dough. Without cancellation, every step finishes its work for a customer who no longer wants the pizza.

Cancellation Travels Down the Chain

Client ──▶ Service A ──▶ Service B ──▶ Database
  │
  └─ cancels the call
        │
        ▼
Service A sees "cancelled" ──▶ Service B sees "cancelled" ──▶ Query stops

The signal flows automatically when each service passes the incoming context to its outgoing calls.

Cancel from the Client

ctx, cancel := context.WithCancel(context.Background())

go func() {
  time.Sleep(2 * time.Second)
  cancel()
}()

reply, err := client.ProcessReport(ctx, req)
// err carries the code CANCELLED when cancel() fires

React on the Server

The server must check the context during long work. A loop that never looks at the context keeps burning CPU after the client leaves.

func (s *server) ProcessReport(ctx context.Context, req *pb.ReportRequest) (*pb.ReportReply, error) {
  for i := 0; i < len(req.Pages); i++ {
    select {
    case <-ctx.Done():
      return nil, status.FromContextError(ctx.Err()).Err()
    default:
    }
    renderPage(req.Pages[i])
  }
  return &pb.ReportReply{}, nil
}

Python Version

def ProcessReport(self, request, context):
    for page in request.pages:
        if not context.is_active():
            return report_pb2.ReportReply()
        render_page(page)
    return report_pb2.ReportReply()

Pass the Context Forward

Reuse the incoming context for every downstream call. A fresh background context breaks the chain, and the downstream service never learns about the cancellation or the deadline.

// Correct: inherits deadline and cancellation
resp, err := inventoryClient.Check(ctx, &pb.CheckRequest{})

// Wrong: severs the chain
resp, err := inventoryClient.Check(context.Background(), &pb.CheckRequest{})

Database Calls

Most database drivers accept a context. Pass it along and the driver aborts running queries on cancellation.

row := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = ?", id)

Cleanup with Defer

Cancelled calls still need tidy endings. Release locks, close files, and roll back transactions in deferred code so that cancellation never leaves half finished work behind.

Common Mistakes

  • Ignoring the context inside long loops
  • Starting background goroutines that outlive the request and never watch the context
  • Replacing the incoming context with a new one
  • Forgetting to call the cancel function after creating a derived context

Status Codes for Context Endings

Two status codes describe a context that ended early. CANCELLED means the caller gave up. DEADLINE_EXCEEDED means the time limit passed. Log both at a low severity, because they describe normal behavior and rarely point to a bug. A sudden rise in either code deserves attention, since it often signals slow dependencies.

Context ends
   ├─ Caller aborts      → CANCELLED
   └─ Time limit passes  → DEADLINE_EXCEEDED

Reading Metadata from the Context

Server handlers read incoming headers through the same context object. Tokens, request IDs, and language settings all arrive this way. Keep the reading code in one helper function so that handlers stay clean.

md, ok := metadata.FromIncomingContext(ctx)
if ok {
  ids := md.Get("x-request-id")
  if len(ids) > 0 {
    log.Printf("request id %s", ids[0])
  }
}

Summary

The context carries deadlines, cancellation, and metadata through every gRPC call. Check it in long running code, pass it to every downstream call, and clean up with deferred statements. Respectful cancellation saves resources across the whole system.