Async Context Propagation
Async context propagation carries request metadata — trace ids, auth, tenants — across task boundaries where thread-locals stop working, via explicit contexts or runtime hooks.
Definition
In thread-per-request systems, thread-locals store per-request state. Async runtimes multiplex many requests over few threads and suspend continuations across queues, so thread-locals become wrong or empty. Context propagation attaches a context object to tasks and restores it at resumption, implemented via async_hooks, contextvars, ExecutionContext, or explicit passing.
Context and scope
Required for tracing, logging correlation, and tenant isolation in Node, Python asyncio, JVM reactive stacks, and Go context pipelines.
Common misconceptions
- Thread-locals work if the runtime is async — suspension moves the continuation to another thread, leaving the thread-local behind
- Propagation is automatic once the framework is async — context loss follows every unawaited boundary, third-party callback, and custom queue hop
- Only observability needs context — authorization scopes, tenant routing, and cancellation deadlines ride the same mechanism
Anti-patterns
- Storing trace ids in thread-locals in a reactive JVM service and shipping spans with missing correlation
- Fire-and-forget tasks launched from an executor without capturing the ambient context
- Propagating mutable context objects shared across concurrent tasks, letting siblings overwrite each other
Related concepts
Related examples
Authoritative source
What it is
A logical request flows through many tasks on many threads. Physical execution context — the thread — no longer identifies the logical one. Context propagation redefines the unit: a context value travels with the async chain, restored at each continuation regardless of the executing thread.
How it works
Mechanisms differ: Node async_hooks intercepts promise and timer callbacks to save and restore storage; Python contextvars copy the mapping at task creation; JVM coroutine context flows through suspend functions; Go passes context values explicitly as first arguments. All require that every hop the continuation takes go through an instrumented boundary.
When to rely on it
Tracing and log correlation across fan-outs, per-tenant storage routing, feature flag resolution, and cancellation deadlines. Test propagation explicitly across each queue and timer boundary in the stack.
Pitfalls
Uninstrumented boundaries — raw thread pools, C libraries invoking callbacks, message queue hops — drop context silently. Mutable shared contexts create cross-request bleed under concurrency. Very large context values copied per task cost allocations; keep them slim and immutable.