The Life of One Request
A buyer clicks "hold seat 14C" and 180 milliseconds later sees a countdown. Following those milliseconds through every layer they touch (the TLS handshake, the load balancer, the socket accept, the parse, the auth check, the pool checkout, the UPDATE, the commit, the serialize, the write back) is the fastest way to learn where a service spends its time and where it can lose the answer. This book returns to this one request in a dozen chapters, and it is worth walking it slowly once.
The numbers below are Stagedoor's, measured from a trace on an ordinary evening, not during on-sale. Roughly 40 milliseconds happen before the process sees a byte, 10 go on parsing and the middleware rings, 90 are spent in the database, 5 on the way out, and 35 are unexplained until Topic 04 explains them.
Before the Process Sees Anything
The browser resolves stagedoor.example, opens a TCP connection to the load balancer, and completes a TLS handshake with it. On a 20-millisecond round trip that is two round trips, the 40 milliseconds the service never sees, before a single byte of the request has been sent, and it is paid per connection rather than per request — which is why keep-alive and HTTP/2 in Chapter 2 matter more to a mobile buyer than any optimization inside the handler. The load balancer terminates TLS, picks api-01, and forwards plain HTTP over the private network. The bytes of POST /events/8812/seats/14C/hold land in a socket buffer on api-01, and the service's clock has not started.
Accept, Parse, Route
The event loop accepts the connection and the HTTP parser turns the bytes into a method, a path, headers and a body. The router matches the path to hold_seat and the framework builds the handler's arguments from the request: event_id from the path, label from the path, the body into a typed model. A body that is not valid JSON is a 400 here; a body that parses but fails the model is a 422 here. Neither reaches domain code. Chapter 3 is about making that boundary the only place untyped data ever exists.
The Rings Around the Handler
Between the router and the handler sit half a dozen middleware rings, and the order they run in is a design decision. The first assigns a request id and starts the request context. The access-log ring records that a request began. The metrics ring starts a timer. The rate limiter checks this buyer's bucket. The authentication ring verifies the bearer token and puts a Principal in the context. Chapter 4 explains why the rate limiter must run before the expensive authentication check, and why the access log has to sit outside both of them, so that it still records the final status when an inner ring short-circuits with a 429.
The Handler and the Store
The handler calls svc.holds.create, the domain function that owns the rule. It checks out a connection from the pool, opens a transaction, locks the seat row, checks the status, updates it to held, inserts the holds row with an expiry ten minutes out, and commits. Ninety of the 180 milliseconds are here, and sixty-one of those ninety are the commit — the moment pg-primary writes the transaction to its write-ahead log and waits for the disk to confirm.
span db.pool.acquire 2 ms # a free connection was waiting span db.tx 88 ms span SELECT ... FOR UPDATE 4 ms span UPDATE seats 3 ms span INSERT holds 2 ms span COMMIT 61 ms # fsync on pg-primary (unattributed) 18 ms # the loop was serving other requests
Read the trace bottom-up and two things stand out. The queries themselves are trivial; the commit is the cost, and it is the cost of durability, which is the one thing nobody wants to give up on a seat hold. And eighteen milliseconds inside the transaction belong to no query at all. That is the event loop switching to other requests between the handler's awaits, and it is the first hint that concurrency in this service is a shared resource rather than a free one.
The Way Back
The domain function returns a Hold. The handler converts it to a response model, the framework serializes it to JSON, the status code is 201, and the bytes are written to the socket. The connection stays open for the buyer's next request. Five milliseconds, and the least interesting part of the picture — until you notice that the work is already committed before the first byte of the response leaves the process.
That gap is the whole reason Chapter 7 exists. If the buyer's Wi-Fi hands off between two access points right here, the hold is real, the seat is held, the countdown is running on the server, and the buyer's screen shows a spinner that will time out. The browser retries. The service sees a second POST for a seat that is now held by the same buyer, and what happens next depends entirely on whether the service can recognize a repeat. Stagedoor could not, and that is how one buyer was charged twice on the night of the spring on-sale.
Where the Time Went
Forty milliseconds of network the service cannot see. Ten of parsing and rings. Ninety in the database, sixty-one of them the commit. Five to answer. That leaves thirty-five milliseconds unexplained, and the honest label for them is "the event loop was busy with somebody else." Under on-sale load that number is the one that grows, because every other request on the instance is competing for the same loop. Topic 04 explains the mechanism; Chapter 14 measures it as loop lag and puts it on the dashboard.
The habit this topic is meant to leave behind is a simple one. Before guessing where a service is slow, ask for the waterfall. Every "the database is slow" that turned out to be a pool wait, a DNS lookup on every request, or a PDF render sitting on the loop cost a week that one trace would have saved — and Chapter 13 shows how to have that trace for every request that crosses a second of latency.
- Measuring only the handler — the buyer's 180 milliseconds and the handler's 90 are both real, and an SLO stated on the wrong one is a promise about a number the buyer never sees.
- Doing work in the handler that the response does not need — rendering the PDF, sending the email, calling a report API; the buyer waits for all of it, and Chapter 8 exists because Stagedoor did exactly this.
- Assuming the response arrived because the handler returned — the hold is committed, the client saw a timeout, and the retry that follows is the double charge of Chapter 7.
- Guessing where the time goes — a week spent tuning queries that took 25 milliseconds while the pool wait took two seconds, because nobody asked for the waterfall first.
- Treating the commit's 61 milliseconds as waste — it is the cost of the hold surviving a crash, and the fix for a slow commit is faster storage or fewer commits per request, never a less durable one.
- Trace one request end to end before optimizing anything, and keep the waterfall as the mental model of the service — the layers are the same for every request it will ever serve.
- Return as soon as the truth is committed, and push everything the response does not depend on to a job.
- Treat a returned handler and a delivered response as two different events, and design the retry for the gap between them.
- Put a deadline on the whole request in the request context, and let every layer spend from that one budget.
- Measure time-to-first-byte at the client separately from handler time at the service, and know which layer each number belongs to before blaming either.
Knowledge Check
Of the 180 milliseconds a buyer waits for a seat hold, roughly 40 happen before the service sees a byte. What are they?
- The load balancer deciding which of the two api instances is less loaded
- DNS resolution plus the TCP and TLS handshakes, paid once per connection
- The HTTP parser turning the request bytes into a method, path and body
- Waiting for a free database connection in the pool before the handler can run
The trace shows 61 of the database's 90 milliseconds inside COMMIT while the queries take under 10. What is the right reading?
- The queries need an index, because a well-indexed transaction should commit in a few milliseconds
- Another request holds the seat row's lock and this transaction is queued behind it for 61 milliseconds
- The commit is the disk write that makes the hold durable, and the cost is real rather than wasted
- The commit is waiting for pg-replica-a to acknowledge the change before it returns to the service
The handler has committed the hold and the buyer's network drops before the response arrives. What is true?
- The service notices the failed write and rolls the transaction back so the seat is available again
- The hold exists and the buyer does not know it, and the retry that follows is the dangerous moment
- TCP guarantees the response will be delivered as soon as the buyer's connection comes back online
- The hold is discarded immediately, because a hold without an acknowledged response is never valid
Eighteen milliseconds inside the transaction belong to no query, and 35 milliseconds of the request belong to no layer. What are they?
- Garbage-collection pauses in the process, which stop every request for the same fraction of time
- Queries the tracing library failed to record, hidden inside the driver's connection handling
- The event loop serving other requests between this request's await points, which grows under load
- Network latency between api-01 and pg-primary, which the trace cannot separate from query time
You got correct