The Request Lifecycle
Every hop between a client and a response, and the fact that each one can fail independently.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind has a problem.
What actually happens between a client sending a request and receiving a response?
A user clicks "Place order" and expects confirmation. Something has to carry that intent across a network, through several pieces of infrastructure, into your code and back.
The client calls the API and the handler runs. Everything between is plumbing that works.
The handler never runs and you have no idea why — the request died at the proxy, at TLS, at the body-size limit, or in a middleware that returned early.
- The handler never runs and you have no idea why — the request died at the proxy, at TLS, at the body-size limit, or in a middleware that returned early.
- The client reports a 504 that appears nowhere in your logs, because the timeout was enforced upstream of your process.
- A request "succeeded" but the client saw a failure: your handler committed, then the connection dropped before the response was written. The client retries and you process it twice (Idempotency in Backends).
What is actually happening
- DNS resolves a name to an address. Cached almost everywhere, and stale entries cause failures that look like your service is down.
- TCP connects, then TLS negotiates. Both cost round trips, which is why connection reuse matters (Keep-Alive and Connection Reuse).
- A load balancer or reverse proxy terminates the connection, picks a healthy instance, and opens or reuses a connection to it. It has its own timeouts, body limits and retry behaviour — often different from yours.
- Your runtime accepts the connection and parses bytes into a request object (Parsing HTTP).
- Middleware runs in order: logging, authentication, rate limiting, validation (The Middleware Pipeline).
- The router matches method and path to a handler (How a Route Becomes a Function Call).
- The handler calls application logic, which touches the database, cache, queue or an external API.
- The response is serialized and written back down the same path — where each hop can still fail.
Every hop is a place to fail
The value of the lifecycle is not memorising the order. It is that each hop is an independent participant with its own configuration, its own timeouts and its own idea of what is too large. A request is a negotiation with every one of them.
When something inexplicable happens, walking the hops in order converts "the API is broken" into "the request never reached the router, so it is the proxy or the middleware above it".
- 1DNS
Resolves the hostname.
fails by Stale or poisoned cache; resolution timeout that looks like the service is down.
- 2TCP + TLS
Opens and secures the connection.
fails by Handshake latency at high rates; expired or mismatched certificate.
- 3Load balancer / proxy
Picks a healthy instance, enforces limits.
fails by Its own timeout (504), body limit (413), or routing to an instance that is unhealthy but still passing checks.
- 4Runtime accept
Accepts the connection, parses HTTP.
fails by Accept backlog full under burst; malformed request; header limits.
- 5Middleware
Logging, authn, rate limit, validation, in order.
fails by Returning early for the wrong reason; ordering that rate-limits after expensive auth (Authenticate First, or Rate-Limit First?).
- 6Router
Matches method + path to a handler.
fails by Precedence surprises; a 404 that is really a route-shape bug (Route Precedence).
- 7Handler + logic
Does the actual work.
fails by Everything else in this domain.
- 8Serialize + write
Turns the result into bytes and writes them.
fails by Client已 gone; payload larger than expected; serialization cost on large results (What Serialization Costs).
The commit-then-disconnect problem
The most instructive failure in the lifecycle is the one with no error anywhere. Your handler commits the order. The connection drops before the response is written. Your logs say success; the client saw a network error; the user presses the button again.
No layer misbehaved. The lesson is that a response is not an acknowledgement, and any write reachable by a client retry needs to be safe to repeat. This is why idempotency is a foundational backend concern rather than an advanced one.
How to build it
Most important first.
- Know which hop owns each timeout. If the proxy times out at 30 s and your handler at 60 s, the second number is decoration (Timeouts).
- Propagate a correlation id from the edge so one identifier follows the request through every layer and every log line (Correlation Ids That Survive Every Hop).
- Assume the response may not arrive. Design writes so a retry is safe.
- Instrument at boundaries, not only inside the handler — the gap between proxy-observed and app-observed latency is where queueing hides.
What can go wrong
- Timeout mismatch: an upstream gives up while your handler keeps working, holding a database connection for a client that has gone.
- Body-size limits enforced at the proxy, producing a 413 your application never sees.
- Header size or count limits rejecting requests after an auth token grows.
- Connection dropped after commit — the write happened, the client believes it did not.
- A health check failing for a reason unrelated to whether the instance can serve traffic (Health Checks: Startup, Readiness, Liveness).
- A client retry can arrive while the original request is still executing — two concurrent executions of the same intent (Duplicate Detection).
- TLS terminates somewhere. Know where, and whether the hop beyond it is encrypted — "we use HTTPS" is often true only to the edge.
- Client IP is only trustworthy if the proxy sets it and you parse the header correctly;
X-Forwarded-Foris client-controllable when unfiltered, which silently breaks IP rate limits (Rate Limiting). - Headers are input. Size, encoding and count are all attack surface.
- "The request reached my server" — reaching the *proxy* is not reaching your process, and most edge failures never produce an application log line.
- "A 504 is my fault." It means something upstream stopped waiting. Your handler may still be running, which is its own problem.
- "The response was sent, so the client got it." Writing bytes to a socket is not delivery.
Operating it
- Compare latency measured at the proxy with latency measured in the handler. A widening gap means requests are queueing before your code runs.
- Log the correlation id at every layer that can generate one, including the proxy, so a client-reported failure can be found at all.
- Count requests that arrive versus responses that complete. The difference is abandoned work.
- Under load the queue moves upstream: your handler looks fast while requests wait to be accepted at all.
- Connection reuse becomes decisive — TLS handshakes at high request rates are pure overhead (Keep-Alive and Connection Reuse).
- More instances mean the load balancer's choices matter: a slow instance still receives traffic until health checks notice.
- Instrumenting every hop costs cardinality and money. Instrument boundaries first; add depth when a boundary points at one.
- Short timeouts fail fast and abandon work that might have succeeded. Long ones hold resources for clients that stopped listening.
Where this applies
Backend advice is context-sensitive. These labels say what each claim is specific to, and where a different stack or scale would differ.
- GENERALThe shape holds for any HTTP backend behind any proxy.
- PROTOCOL-SPECIFICHTTP/1.1 uses one request per connection at a time, so head-of-line blocking is per-connection; HTTP/2 multiplexes streams over one connection, which changes what "connection reuse" buys and moves head-of-line blocking down to TCP.
- CLOUD-SPECIFICManaged load balancers impose their own idle and request timeouts that you cannot exceed from inside the application — the exact ceilings differ by provider and product.
Where the depth lives
This domain teaches the application-side mechanism and hands the rest off.