Skip to content

Deadlines & Cancellation

A deadline is an absolute point in time — “give up if this isn’t done by 10:00:03.500”. A timeout is a duration — “give up after 300ms” — which most libraries convert into a deadline internally.

The distinction matters because gRPC propagates the deadline, not the timeout. When service A calls B, which calls C, the same absolute deadline travels down the chain. Each hop knows how much wall-clock time is actually left, so nobody keeps working on a request the caller has already given up on.

flowchart LR
  a["Service A
deadline = now + 300ms"] -->|deadline propagated| b["Service B
sees ~280ms left"]
  b -->|deadline propagated| c["Service C
sees ~250ms left"]
  c -. "if deadline passes,
all hops abort" .-> x["DEADLINE_EXCEEDED"]
One absolute deadline travels down the whole call chain

The most important operational rule in gRPC: every call gets a deadline. A call without one waits forever by default. One slow or stuck dependency then ties up a request, which ties up a goroutine/thread/connection, and under load that cascades into a total stall — the classic way a single slow service takes down everything upstream.

Here is how you set a deadline on a call:

ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
defer cancel() // always release the context
user, err := client.GetUser(ctx, &userv1.GetUserRequest{Id: 42})
if status.Code(err) == codes.DeadlineExceeded {
log.Println("call ran out of time")
}

Deadlines are one way a call ends early; explicit cancellation is the other. If the client goes away — the user closed the tab, the parent request was itself cancelled — gRPC signals the server that the call is cancelled. A well-written server checks for cancellation and stops: it aborts the database query, breaks the loop, and frees resources instead of computing a result nobody will read.

On the server side you watch the request context:

func (s *server) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.User, error) {
select {
case <-ctx.Done(): // client cancelled or deadline exceeded
return nil, status.FromContextError(ctx.Err()).Err()
default:
}
// ... do the work, ideally passing ctx to the DB call too
return lookup(ctx, req.Id)
}

The golden rule: propagate the context. Pass the incoming request’s context into every downstream gRPC call and database query. Then one deadline or one cancellation cleanly unwinds the entire tree of work.

What does gRPC propagate down a call chain?
What happens to a gRPC call with no deadline set?
What status code signals a call ran out of time?
How does a server avoid computing results nobody will read?