Deadlines, not timeouts — deadline propagation with the grpc-timeout header
A timeout is relative and resets at every hop; a deadline is one absolute point in time that propagates, so each hop passes the next only the time remaining — which is what stops a downstream service from working for a caller who already gave up.
We saw one server keep computing 'Hello Ada' long after its wire died. Now stretch that across an A → B → C chain: without a shared clock, the deepest service keeps burning CPU for a request its caller abandoned a moment ago.
Scene 06
Deadlines, not timeouts
- Watch
- Try it
- Predict
- Capture
A network call usually rides under a patience limit: 'give up if no answer comes back in time.' The naive way to express that is a TIMEOUT — a relative duration like 'fail after 1 second.' The catch is that a timeout RESETS at every hop: A waits 1s on B, but when B calls C, B starts C's clock fresh at another full second — so the chain as a whole can wait far longer than anyone intended. The fix is a DEADLINE: instead of a duration, you compute one absolute point in time ('give up at 12:00:01.000') and PROPAGATE it. Each hop subtracts the time already spent and hands the next hop only the remaining time. Watch greet("Ada") travel A → B → C under a 1000ms deadline: the budget bar up top shrinks as time is spent, and the grpc-timeout chip on each arrow shows the smaller 'time left' — one absolute clock, re-expressed per hop.
Highlighted lines are the ones running in the diagram right now.
def call(stub, req, ctx):# remaining time on the ONE shared clock, not a fresh durationremaining = ctx.deadline - now()headers = {'grpc-timeout': encode(remaining),}return stub.invoke(req, headers)
def handle(req, headers):if 'grpc-timeout' in headers:# honor the caller's clock — bind to the same momentctx.deadline = now() + decode(headers['grpc-timeout'])else:# no propagation: start a fresh full timeout, alonectx.deadline = now() + DEFAULT_TIMEOUTresult = doWork(req, ctx)return result
def doWork(req, ctx):while not done:if now() >= ctx.deadline:raise DEADLINE_EXCEEDED # stop, caller is gonestep() # one slice of the cWorkMs of workreturn Reply(greeting=greet(req.name))
Where this sits in Build a gRPC-style RPC framework
Scene 06 of 14, in the Reliability act — Deadlines, cancellation, retries, and the interceptor onion.. A timeout is relative and resets each hop; a deadline is absolute and propagates the time remaining — so a downstream service never works for a caller that already gave up.
Up next. A deadline handles 'time ran out,' but a client often quits for a reason that has nothing to do with the clock — the user closed the tab — and the same downstream services need to hear 'never mind' the instant it happens.
All 14 scenes in Build a gRPC-style RPC framework · Every curriculum