Why does per-request context keyed to the running thread break in event-loop or coroutine web frameworks?
answer
- thread identity is not request identity
- one thread interleaves many requests
- resumption may land elsewhere
- missing is loud, leaked is silent
- clear on exit, propagate on handoff
basics
~20 sThread-keyed storage assumes one thread serves one request end to end. Under loop or coroutine execution a thread interleaves many requests and a request touches several threads, so context goes missing after a resumption, or belongs to somebody else.
solid answer
~40 sRequest ids, the authenticated principal, tenant and trace state are often kept in storage attached to the executing thread, so any code on the stack reads them without a parameter. That depends on the thread-per-request assumption. On a loop thread, consecutive turns belong to **different requests**, so a value left behind by one is visible to the next. In a coroutine framework the handler may resume on a **different carrier thread**, where the value is simply absent. So the shapes are a *missing* context (no request id, a null principal) and a *leaked* one, far worse, because a check then reads another request's identity. Make context travel with the request: carry it on the request object, propagate it at each hop, and clear thread-attached storage in a finally block.
go deeper
Recall that ambient per-request values are stored against the running thread, and that this only holds when a single thread serves the whole request.
Explain the three ways it breaks — interleaving on a shared loop thread, migration across a suspension, and handoff to another pool — and how context is instead carried with the request.
Diagnose the silent version: two identities in one request's logs, a defect visible only under load, and the security consequence of a stale principal reaching an authorisation check.
Decide the standard: whether identity and tenancy are passed explicitly rather than ambiently, and how propagation is enforced at every boundary the organisation's services share.
## Why the pattern exists at all Ambient per-request state is a convenience: instead of passing a request id, principal, tenant, locale and trace span through every method signature between the handler and the data-access layer, frameworks let you put those values in storage attached to the currently running thread. Any code on the stack can read them. Logging layers pick them up automatically, authorisation helpers read the principal, and clients attach trace headers without being told which request they belong to. The hidden assumption is that **the running thread identifies the request**. That is true in a thread-per-request model and false in the other two. ## How each model breaks the assumption - **Interleaving.** A loop thread runs a turn for request A, then a turn for request B. Any value left in thread-attached storage after A's turn is visible to B. There was no handoff for the framework to clear. - **Migration.** A suspending handler releases its carrier thread at a suspension point and may be resumed on another one. The context written before the suspension is on the old thread; the code after it reads the new thread and finds nothing. - **Handoff to other pools.** Work passed to a separate execution pool — a background task, a callback completed by a client library's own thread — runs somewhere the context was never installed. - **Reuse without cleanup.** Even in the thread-per-request model, workers are pooled, so a value not cleared at the end of a request is still there when the same worker picks up the next one. ## The two failure shapes, and why one is much worse | Shape | Symptom | Severity | |---|---|---| | Missing context | Log lines with no request id, traces that start a new root mid-request, a null principal in a helper | Noisy and confusing, rarely dangerous | | Leaked context | Lines attributed to the wrong user, a trace stitched to another request, an authorisation check reading a stale principal | Potentially a security defect | The leak is the one to fear. A missing value usually fails loudly — something is null and a check rejects the request. A leaked value fails **silently and plausibly**: the code reads a perfectly valid principal that belongs to a different user, and a permission check may pass that should have failed. This is why cleanup is not hygiene but correctness, and why a security review of a multi-tenant service should ask how tenant identity reaches the data layer. ## Diagnosing it 1. **Look for the discontinuity, not the error.** Find a request whose log lines carry two different identifiers, or whose identifier vanishes at a specific point. That point is almost always the first suspension, callback boundary or handoff to another pool. 2. **Compare load levels.** Under light load, a loop thread often happens to run a whole request's turns consecutively and the bug hides; under load, interleaving becomes frequent and the mismatch appears. A defect that only shows in production traffic fits this shape. 3. **Assert it in tests.** Run two overlapping requests through the handler chain with different identities and assert that each observation of the context matches the request that produced it. A single-request test will pass no matter how broken the propagation is. ## Making context travel with the request - **Carry it on the request object.** The framework already hands every stage a per-request object; attaching context there means the value follows the request wherever it runs, with no thread involved. - **Pass it explicitly** across module boundaries. Verbose, but immune to every mechanism above, and it makes the dependency visible in the signature — often the right choice for identity and tenancy specifically. - **Use the runtime's propagation mechanism.** Event-loop and coroutine runtimes generally offer a way to associate values with a task or continuation so they are restored automatically when the work resumes elsewhere. This is the mechanism that keeps the ambient style working; it must be installed on every handoff, including into client libraries. - **Always clear on exit.** Whatever you install on a thread, remove it in a finally-style block. A pooled thread that leaves a request's values behind is a leak waiting for the next request. - **Never cache a context value in a shared field.** A value read once and stored on a shared object outlives the request entirely, which is the same bug without the thread. ## What to say in an interview State the assumption first — thread identity equals request identity — then name the three ways execution models break it (interleaving, migration, handoff), then the two symptoms, then the fix. Finishing with the security angle, that a leaked principal is more dangerous than a missing one, shows you have lived with the failure rather than read about it.
- Why is a leaked context worse than a missing one?A missing value usually fails loudly: something is null and a check rejects the request. A leaked value is a valid identity belonging to somebody else, so logs, traces and even authorisation decisions look plausible while being wrong. That makes it a silent correctness and security defect.
- Does the problem disappear in a thread-per-request framework?The interleaving and migration cases do, but reuse does not. Workers are pooled, so a value never cleared at the end of a request is still present when that worker serves the next one. Installing in a try block and removing in a finally block is required in every model.
- Why does this defect often appear only under production load?With little traffic a loop thread tends to run one request's turns back to back, and a resumption often lands on the thread that started, so the wrong value is rarely observed. Concurrency makes interleaving and migration common, which is why overlapping-request tests catch what single-request tests cannot.
saying these in an interview costs you the question
- Assumes the handler always resumes on the thread that started it
- Treats a lost request id as cosmetic rather than a propagation bug
- Installs thread-attached context without clearing it on exit
- Caches the current principal in a shared object field
- Tests propagation with one request at a time only