A deadline is an absolute point in time the whole request must finish by — and it travels across every hop. That one property fixes a class of outages timeouts can't.
Say the frontend allows 5s for a request. It calls service A, which calls B, which calls C. With per-hop timeouts, each hop starts its own 5s clock — so the total budget silently balloons to 15s+, and C may keep grinding on work the user abandoned 10 seconds ago.
With a deadline, the frontend stamps "finish by T = now + 5s" and sends it down. Every hop subtracts the time already spent. When T passes, everyone stops at once.
gRPC carries the remaining time in the grpc-timeout request header on each call. A well-behaved service passes the incoming context's deadline down to its own outbound calls, so the budget shrinks monotonically along the chain instead of resetting.
In most languages you pass a timeout at the call site and the library converts it to an absolute deadline on the wire:
import grpc
try:
# 'timeout' seconds -> becomes an absolute deadline internally
resp = stub.GetUser(req, timeout=0.5)
except grpc.RpcError as e:
if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
# the 500ms budget elapsed — do NOT blindly retry with the same budget
log.warning("user service too slow")
raise
The deadline is only useful if the server actually stops. Long handlers should check the context and bail early — otherwise you burn CPU producing a response no one will read.
def ExportRows(self, request, context):
for row in db.iter_rows(request.query):
# client gave up, or the deadline passed? stop now.
if not context.is_active():
context.cancel()
return
yield to_proto(row)
context becomes inactive. Pass that same context into your downstream calls and the cancellation flows through the whole tree — nobody keeps working on an abandoned request.
| Concern | Per-hop timeout + retry | Propagated deadline |
|---|---|---|
| Total latency bound | Multiplies down the chain | Fixed end-to-end |
| Wasted work | Deep services keep computing | Everyone stops together |
| Retry storms | Each layer retries → amplification | No budget left → no retry |
| User experience | Unbounded tail latency | Predictable p99 ceiling |
UNAVAILABLE-only retries and jittered backoff (see Status Codes) so a struggling dependency doesn't get hammered by its own callers.
is_active() in long-running handlers and streams so cancellation actually frees resources.DEADLINE_EXCEEDED — treat it as "over budget," not "retry immediately."