Deadlines & Cancellation
A deadline, not a timeout
Section titled “A deadline, not a timeout”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"]
Always set one
Section titled “Always set one”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")}try: # timeout is converted to an absolute deadline internally user = client.GetUser(user_pb2.GetUserRequest(id=42), timeout=0.3)except grpc.RpcError as e: if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED: print("call ran out of time")const deadline = new Date(Date.now() + 300); // absolute timeclient.getUser({ id: 42 }, { deadline }, (err, user) => { if (err?.code === status.DEADLINE_EXCEEDED) { console.log('call ran out of time'); }});Cancellation stops wasted work
Section titled “Cancellation stops wasted work”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)}def GetUser(self, request, context): if not context.is_active(): # client cancelled / deadline passed return user_pb2.User() # pass context to downstream calls so cancellation propagates return lookup(request.id, context)getUser(call, callback) { if (call.cancelled) return; // client already gave up call.on('cancelled', () => stopWork()); doLookup(call.request.id, callback);}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.