Skip to main content
cd ../lexicon
sys/docs/lexicon/deadline-propagation.md
Lexicon

Deadline Propagation

Difficulty: Advanced
3 min read

In plain English

Plain definition

Passing down the caller's remaining time budget through every service hop instead of giving each hop its own fresh timeout, so nobody keeps working on a request the original client already gave up on.

Deadline propagation works by attaching an absolute expiry timestamp (or a remaining-duration delta) to the request context and re-deriving it at every hop rather than resetting the clock. In gRPC this is the grpc-timeout header, computed relative to wire time at each proxy or service boundary; in HTTP-based systems it is typically a custom header like X-Deadline carried through context.Context in Go or Deadline objects in Java. The critical invariant is that a service must subtract its own processing time and any queueing delay before forwarding the deadline downstream — if a service instead issues a fresh full-length timeout to its dependencies, the effective end-to-end latency budget becomes unbounded despite the caller having already timed out.

The failure mode this prevents is orphaned work: a caller cancels or times out, but downstream services, unaware of the abandoned context, continue executing expensive queries, holding locks, or fanning out further RPCs. Under load this compounds into cascading saturation, because retries from the now-timed-out caller pile new requests on top of stale in-flight ones still consuming CPU and connection pool slots. Proper propagation lets intermediate services check ctx.Err() == context.DeadlineExceeded before starting non-trivial work and abort early, freeing resources immediately rather than after full execution.

  • Clock skew: absolute deadlines require reasonably synchronized clocks (NTP/PTP); large skew between hosts can cause premature or delayed expiry relative to the sender’s intent.
  • Budget subdivision: fan-out calls must divide the remaining budget across parallel branches, or a single slow branch can starve the deadline for siblings that haven’t even started.
  • Retry interaction: naive retry logic that resets the timeout per attempt defeats propagation entirely; retries must consume from the same remaining budget, not restart it.
  • Observability gap: without deadline propagation, distributed traces show downstream spans succeeding long after the client-facing span already returned an error, making root-cause analysis of tail latency incidents far harder.

Service meshes like Istio/Envoy and RPC frameworks like gRPC implement this natively at the transport layer, but propagation breaks silently the moment a hop crosses an async boundary — message queues, background job schedulers, or fire-and-forget goroutines/threads have no inherent concept of a caller’s deadline unless the application explicitly serializes and re-hydrates it. This makes deadline propagation an architectural discipline as much as a library feature: every internal client wrapper, ORM call, and cache lookup must be deadline-aware, or the guarantee degrades at exactly the boundary where it’s most needed.

Architecturally, deadline propagation shifts timeout management from a per-service local concern to a global, chain-wide contract, which is why it must be enforced consistently across every service mesh sidecar, RPC stub, and async worker in the call graph; a single non-compliant hop reintroduces the unbounded-latency and orphaned-work failure modes it was designed to eliminate.