The lifecycle of a request
Everything that happens between the user's click and the response, where the time goes and where things break.
The problem
Section titled “The problem”On your machine, fetch('/api/links') responds in 15 ms. In production, the same endpoint takes 600 ms for a user in Recife, Brazil, and 2 s when the database is busy. The code is identical. What changes is the path the request travels.
Locally, the browser, server and database all sit on the same computer. In production there are at least six hops between the click and the response, and each one costs time and can fail.
The full path
Section titled “The full path”sequenceDiagram autonumber participant B as Browser participant D as DNS participant C as CDN participant L as Load balancer participant A as App participant P as Postgres B->>D: linkr.app? D-->>B: IP B->>C: TLS + GET C->>L: cache miss L->>A: healthy instance A->>A: middlewares A->>P: SELECT (via pool) P-->>A: rows A-->>L: JSON L-->>C: response C-->>B: response
| Step | What it does | What can go wrong |
|---|---|---|
| DNS | Translates the name into an IP | Long TTL after switching servers; DNS provider outage |
| TCP + TLS | Opens the connection and negotiates encryption | Expired certificate; high latency for distant users |
| CDN / Edge | Serves cached content close to the user | Caching what it shouldn’t (one user’s data served to another) |
| Load balancer | Spreads traffic across instances and drops unhealthy ones | Health check that lies; timeout shorter than the app’s |
| App | Middlewares, business logic, serialization | CPU work blocking the event loop; state kept in memory |
| Pool + database | Reuses connections and runs queries | Exhausted pool; query without an index; locks |
Where the time goes
Section titled “Where the time goes”For a first request from a user in São Paulo to a server in Virginia (US), the rough numbers look like this:
Three practical conclusions:
- The network usually costs more than the code. Putting the server and database close to your users (or using a CDN) pays off more than micro-optimizing a function.
- Reused connections are cheap. After the first handshake, HTTP keep-alive removes TCP and TLS from the following requests. The same goes for the database connection pool.
- Every database round trip adds up. With the database 1 or 2 ms away, 10 queries cost little; 300 queries in a loop cost half a second. This comes back in lesson 1.3.
In practice: measuring each step
Section titled “In practice: measuring each step”On the client, with curl
Section titled “On the client, with curl”curl -o /dev/null -s -w ' dns: %{time_namelookup}s tcp: %{time_connect}s tls: %{time_appconnect}s first byte: %{time_starttransfer}s total: %{time_total}s' https://linkr.app/api/healthThe times are cumulative. If first byte minus tls is large, the bottleneck is on the server; if tcp is already high, it’s distance or the network.
On the server, with Server-Timing
Section titled “On the server, with Server-Timing”The Server-Timing header shows up in Chrome DevTools under Network → Timing. It’s a cheap way to see, per request, where the server spent its time:
app.get('/api/links', async (req, res) => { const start = performance.now();
const links = await db.query( 'SELECT id, slug, url FROM links WHERE user_id = $1', [req.user.id], ); const dbMs = performance.now() - start;
const body = JSON.stringify(links.rows); const totalMs = performance.now() - start;
// Shows up in DevTools as two bars: "db" and "total". res.setHeader('Server-Timing', `db;dur=${dbMs.toFixed(1)}, total;dur=${totalMs.toFixed(1)}`); res.type('json').send(body);});To measure every route without repeating code, a middleware using the on-headers package can add the header right before the response goes out.
Pitfalls
Section titled “Pitfalls”- Always testing “warm”. The first request (DNS, handshake, empty pool, cold cache) is the one a new user feels. Measure both.
- Misaligned timeouts. If the load balancer gives up at 30 s and your app keeps working for 60 s, you pay for the work and nobody gets the response. Each layer’s timeout should be shorter than the one above it.
- What AI tends to generate: handlers that make several independent calls in sequence (
await a(); await b(); await c();) when they could run in parallel withPromise.all, and no timeout at all on calls to external APIs.
Go deeper
Materials in English unless noted.
- ArticleWhat happens when…alex/what-happens-when
The full path from typing a URL to the page appearing, in exhaustive detail.
- BookHigh Performance Browser NetworkingIlya Grigorik
Chapters 1 to 4 explain latency, TCP and TLS with the numbers that matter.
- ArticleLatency numbers every programmer should knowJonas Bonér, after Jeff Dean
The classic table of orders of magnitude: memory, disk, local network, across continents.
- Docs
- ArticleWhat is a CDN?Cloudflare Learning
A short introduction to CDNs, the edge and caching close to the user.