skip to content

In a framework's dispatch pipeline, when does one request's lifetime end, and why can work a handler starts outlive it?

level: seniorimportance: should knowfreq 44%

answer

  1. the window closes at release
  2. the framework waits only for what it scheduled
  3. recycled state becomes someone else's
  4. copy values, never the request
  5. missing correlation id is the tell

basics

~20 s

A request's lifetime runs from the hand-off into the pipeline until dispatch completes and its resources are released. Work handed to a separate executor is not bound to that window, so it can outlive the request object and its per-request context.

solid answer

~50 s

The lifetime begins when the server hands the parsed request to the pipeline and ends once the response is fully written and the framework runs its release step: per-request context cleared, the body and any borrowed resources closed, and buffers possibly returned to a pool. Everything the framework itself schedules — hooks, binding, the handler, rendering — is inside that window, and if a handler returns an asynchronous result the framework extends the window until that result completes. What escapes is work the handler starts on some other executor and does not report back: the pipeline has no idea it exists, so dispatch ends underneath it. From that moment the request object may have been recycled and the per-request context cleared, so reading the body, the path variables or a context value from the escaped work yields nothing, stale data, or — if the carrier is reused — another request's values.

go deeper

for a junior

Know that the request stops being usable once its response has been written. If you need a value later, copy it out into your own variable while the handler is still running.

for a middle

Explain the release step and pooling: state is not merely stale after the request ends, it may have been reused by another request. Distinguish an awaited asynchronous result from an untracked handoff.

for a senior

Recognise the symptoms — missing or wrong correlation ids, intermittent closed-stream errors, cross-tenant reads under load — and fix them by copying values at the handoff and propagating context explicitly.

for a principal

Set the policy for work that outlives a request: durable handoff with its own retries versus in-process tasks, and how the shutdown sequence accounts for work the pipeline never knew about.

## The window A request's **lifetime** is the interval in which the framework guarantees that the request's state is valid: it opens when the server adapter hands a parsed request into the pipeline and closes after the last stage has run and the release step has completed. Inside that window the request object, its body, its path and query values, and anything stored in the **per-request context** are all real. Outside it, none of that is promised. The release step is what makes the boundary sharp. Frameworks typically use it to close or drain the body, release connections and other resources acquired by hooks, clear the per-request context, and return pooled buffers or request objects for reuse. Reuse is the sting: the memory does not become invalid, it becomes **someone else's**. ## What the framework keeps alive, and what it does not | Work | Inside the lifetime? | Why | |---|---|---| | Hooks, binding, handler, rendering | Yes | The pipeline schedules them itself | | An asynchronous result the handler returns | Yes | The framework waits for it before finishing dispatch | | A task handed to a separate executor and awaited | Yes | The dispatch is still blocked on its completion | | A task handed off and not awaited | **No** | Nothing ties it to dispatch, so dispatch finishes underneath it | | A callback registered on completion | Yes, and always | It is defined as part of the closing sequence | The distinction is not "synchronous versus asynchronous" — plenty of frameworks keep a request alive across an asynchronous result perfectly well. The distinction is **whether the pipeline knows it has to wait.** ## The three ways escaped work bites 1. **The body is gone.** Bodies are commonly read-once streams over a connection that has since been reused for another request. Reading one after dispatch ends either raises or returns nothing. 2. **The per-request context is empty — or wrong.** Correlation ids, identity and tenant markers usually live in a carrier attached to the executing worker or to the call. Escaped work either runs where the carrier was never populated (values missing, logs losing their correlation id) or runs on a worker whose carrier has since been filled by a different request (values present but belonging to someone else). The second failure is worse, because nothing looks broken. 3. **Resources were released.** A unit of work opened by a hook is closed on the way out. Escaped work that still holds a reference to it operates on something already finished. ## What to do instead - **Capture values, not the request.** Copy the few fields the background work needs — an id, a tenant, a correlation id — into a plain immutable object before handing off. Never pass the request, the response, the body, or a context accessor across the boundary. - **Propagate context explicitly.** If the escaped work must log with the same correlation id, pass it and set it up on the other side; do not assume an ambient carrier follows the handoff. - **Make the handoff durable when it matters.** Work that must not be lost belongs in a queue or an outbox written inside the request's unit of work, not in an in-process task that dies with the process. "Fire and forget" quietly means "lose on restart", and also means the shutdown sequence has no idea it should wait. - **Return the asynchronous result if you can.** If the request genuinely depends on the work, returning it keeps the request inside its own lifetime and gives you the framework's error handling and timeouts for free. ## How the risk varies with the execution model Frameworks differ in how the lifetime is anchored. Where each request occupies a worker for its duration, per-request state is often attached to that worker, and the danger is a handoff to a pool whose workers carry other requests' values. Where many requests are interleaved on a small number of workers, per-request state has to travel with the call itself, and the danger is a handoff that drops the carrier entirely so values simply disappear. The defensive rule is identical in both: copy what you need at the moment of handoff. ## Symptoms that point straight here Log lines from background work missing their correlation id, or carrying one that belongs to a different request. An occasional "stream closed" or "already completed" error on a path that usually works. A tenant-scoped query that returns the wrong tenant's rows only under load. All three are the same shape: something read per-request state after the request that owned it had ended.

  • Why is a background task that silently reads the wrong tenant worse than one that throws?
    Because it produces plausible output. A closed-stream error is loud and fails fast; a per-request carrier refilled by another request returns a valid-looking value, so the work completes, the logs look healthy, and the damage is discovered later as data attributed to the wrong tenant.
  • Does returning an asynchronous result from a handler extend the request's lifetime?
    Yes. The framework treats dispatch as unfinished until the result completes, so the request, its context and its resources stay valid, and the framework's own timeout and error handling still apply. It is the handoff the framework is never told about that escapes.
  • How should work that must survive the response be structured?
    Record the intent durably inside the request's unit of work — a queued message or an outbox row — and let a separate consumer do the work with its own context and its own retries. That decouples it from both the request's lifetime and the process's.

It is like taking notes from a library book during a meeting and then quoting it an hour later: the book has been reshelved and lent to someone else, so whatever is now on that shelf is not what you read.

saying these in an interview costs you the question

  • Assumes the request object stays valid as long as a reference exists.
  • Expects per-request context to follow work onto another executor automatically.
  • Treats fire-and-forget as durable rather than lost on restart.
  • Reads the body from background work after the response was written.
  • Thinks an asynchronous handler result already means the request has ended.