Skip to content
M01Local is not productionLesson 1 of 4

The lifecycle of a request

Everything that happens between the user's click and the response, where the time goes and where things break.

Goal
Sketch from memory the full path of a request in production, know where the time is spent and measure each step.

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.

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

For a first request from a user in São Paulo to a server in Virginia (US), the rough numbers look like this:

First request, São Paulo → us-east-1 (illustrative values)
DNS (uncached)40 ms
TCP handshake120 ms
TLS 1.3 handshake120 ms
Request round trip120 ms
Your code + 10 queries35 ms

With an RTT of ~120 ms between Brazil and the US East Coast, the network dominates. The code you optimize is the smallest bar.

Show data
DNS (uncached)40 ms
TCP handshake120 ms
TLS 1.3 handshake120 ms
Request round trip120 ms
Your code + 10 queries35 ms

Three practical conclusions:

  1. 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.
  2. 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.
  3. 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.
Terminal window
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/health

The 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.

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.

  • 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 with Promise.all, and no timeout at all on calls to external APIs.

Go deeper

Materials in English unless noted.