skip to content

Why does work deferred past the response often fail when it touches a request-scoped object or resource?

level: juniorimportance: should knowfreq 50%

answer

  1. lifetime ends with the response
  2. pooled objects come back reused
  3. closed stream, returned connection
  4. copy the values you need
  5. fails only under concurrency

basics

~20 s

Request-scoped objects — request and response, body streams, per-request context, a borrowed connection — are torn down or returned to a pool when the response ends. Deferred code holding a reference finds them closed, cleared, or already serving another request.

solid answer

~50 s

A framework creates a set of objects for the duration of one exchange and disposes of them when the response ends: the request and response objects, the body stream, the per-request context bag, and anything borrowed from a pool on the request's behalf. Work that outlives the response but captured one of those references is reading something whose lifetime is already over. The symptoms vary — a closed stream, an empty context, a connection that now belongs to a different request — and they are load-dependent, because a single-request test usually leaves the object intact long enough to get away with it. The fix is not to extend the lifetime; it is to **copy the plain values you need** — identifiers, the caller's identity, the entity keys — into the deferred task before you respond, so it holds nothing the framework manages.

go deeper

for a junior

Learn the lifetime rule: objects the framework gives your handler are valid for that one request. Copy the values you need out of them before you respond.

for a middle

Explain the mechanics — one-shot body streams, connections returned to a pool, context containers cleared or recycled — and why the third case corrupts data instead of throwing.

for a senior

Recognise the signature in an incident: intermittent, load-correlated failures or data attributed to the wrong caller, and drive the fix toward value snapshots rather than longer-lived resources.

for a principal

Make it structural. If deferred work can only be handed an immutable command object, this class of bug cannot be written, and no reviewer has to spot it by eye.

## What "request-scoped" means A server-side web framework builds a small world for each incoming exchange and demolishes it when the exchange is over. Typical inhabitants of that world are: - the **request object** and its parsed pieces — headers, path values, query values; - the **body stream**, which is usually readable exactly once; - the **response object or writer** that the handler writes through; - a **per-request context bag** that stages in the chain use to pass values along; - anything **borrowed on the request's behalf** — a database connection, an outbound client lease, an open file, a transaction. Creating these per request is what makes them safe to use without synchronisation: only one exchange can see them. The same property is what makes them dangerous to keep. Their lifetime is defined by the framework, not by who is still holding a reference. ## The three ways the reference goes bad 1. **It was closed.** Streams and writers are closed when the exchange ends. A later read or write fails, or quietly returns nothing at all. 2. **It was returned.** A borrowed connection goes back to its pool at the end of the request. Using it afterwards means using a resource that the pool believes is free, and that may already be executing another request's statements. 3. **It was cleared or recycled.** Some frameworks reuse the per-request containers themselves. A deferred task reading one after the exchange finished may find it empty, or filled with a different request's values. The third case is the nastiest, because it does not throw. It produces plausible, wrong data — an audit line attributed to the wrong caller, a background action performed for the wrong account. ## Hold values, not handles | Do not capture | Capture instead | |---|---| | The request or response object | The specific header, path or query values you need, as plain strings | | The body stream | The already-parsed and validated value object | | The per-request context bag | The identifiers you read out of it, copied while the handler was running | | A borrowed connection or transaction | Nothing — let the deferred work acquire its own, or make the work a separate unit | | The caller's live session or connection | The caller's identifier, recorded during the request | The general rule: a deferred task should hold only **immutable values it fully owns**. Anything whose lifetime is managed by the framework has a clock on it that the deferred task cannot see. ## Why it passes tests and fails in production This bug hides from ordinary tests. A test typically issues one request, waits, and then inspects the deferred effect, with nothing recycling the object in between — so the reference is often still usable. Under real traffic the framework has already closed the stream, returned the connection, and started another exchange on the same objects. The result is intermittent failures that correlate with load rather than with input, which is why they are so often misdiagnosed as a race in the application's own code rather than as a lifetime mistake. A second reason it survives review: the code *looks* correct. Capturing a reference and using it later is ordinary programming. Nothing in the syntax signals that this particular object has an owner with different ideas about when it dies. ## Making the mistake hard to repeat - Build a small **value object** during the handler — correlation id, actor id, entity keys, the parsed command — and hand only that to the deferred work. - Treat any framework-supplied object as **borrowed**, like a loan you return at the door. - If the deferred work needs data it did not copy, have it **re-read from storage** by identifier, rather than keeping a reference so it can look again. - Let the deferred work **acquire its own resources**, with its own timeout and its own error handling, so nothing it uses is tied to a finished exchange. ## The one-line summary The lifetime of a request-scoped object ends with the response, not with the last reference to it. If work is going to outlive the response, it must be handed data, never handles.

  • Deferred work needs the caller's identity and the request identifier. What exactly should you pass?
    Immutable copies taken while the handler was still running: the identity string, the correlation identifier, the entity keys. Put them in a small value object the deferred task owns outright, so nothing it holds has a lifetime the framework controls.
  • Why does this bug usually pass tests and fail in production?
    A test runs one request and then the deferred task, with nothing recycling the objects in between, so the stale reference often still works. Under load the stream is already closed and the connection already returned, possibly to another request, so failures are intermittent and correlate with traffic rather than input.

saying these in an interview costs you the question

  • Assumes a reference stays valid as long as something holds it.
  • Captures a borrowed connection and reuses it after the response.
  • Tries to read the request body inside deferred work.
  • Calls it a race in application code rather than a lifetime mistake.
  • Fixes it by holding the request open until the deferred work finishes.