Networking
Measuring a request
How to read a browser waterfall, send Server-Timing headers, follow a request with distributed traces, and break down the 60 ms of server time for a product page.
Saying "the page is slow" is not a diagnosis. Browser tools split a request into phases. Server headers and traces split the server's share into its parts. Together they turn "it's slow" into "the recommendations call is slow".
Concept
Before you start: TTFB (time to first byte) is the time from sending a request to receiving its first byte. It contains one round trip plus all server time. A span is one timed step in a trace, with a start time and a duration.
Walking through it: the browser waterfall
Open Chrome DevTools, go to the Network tab, reload, and click a request's Timing tab. Each phase maps onto the earlier pages:
| DevTools phase | What it is | Large when |
|---|---|---|
| Queueing / Stalled | Waiting for a connection slot, or behind higher-priority requests | Too many requests on HTTP/1.1, or a busy main thread |
| DNS Lookup | Name resolution | Cold cache, slow resolver, long CNAME chains |
| Initial connection | TCP handshake (or QUIC) | Far from the server, no CDN |
| SSL | TLS handshake | Same, plus missing session resumption |
| Request sent | Uploading the request | Large bodies or cookies |
| Waiting for server response (TTFB) | One round trip plus all server time | Slow backend, cache miss at the CDN |
| Content Download | Receiving the body | Large or uncompressed responses, slow links |
A reused connection shows no DNS, Initial connection or SSL bars. That is the visual sign that keep-alive is working. A staircase of requests, each waiting on the one before, points to dependency chains. HTML discovers CSS, which discovers a font. Preload hints or inlining can break those chains.
Walking through it: Server-Timing
To see the server's share inside TTFB, send a Server-Timing header. DevTools shows it in the same Timing tab:
Server-Timing: db;dur=8.1, recs;dur=24.7, render;dur=11.9, total;dur=59.6Each entry is a named duration in milliseconds. It is cheap to add, and it tells you which part of the server to look at first.
Walking through it: distributed traces
For the full picture, use distributed tracing (OpenTelemetry, exported to Jaeger, Tempo, Honeycomb or Datadog). Each hop creates a span. A traceparent header carries the trace ID across service boundaries.
The trace for our product page would show the load balancer span, the app span, and child spans for the Redis calls, the SQL query and the recommendations call. They sit on a timeline. Gaps between spans matter as much as the spans. An unexplained 15 ms gap is often a queue, a lock or a GC pause. See observability.
Inside the data center: the server's 60 ms
Here is a realistic breakdown of the 60 ms of server time for the product page:
Load balancer (L7 routing, maybe TLS re-encrypt) 1 ms
Framework: parse request, middleware, auth check 4 ms
Redis GET session + GET product:42 (2 × ~0.5 ms) 1 ms
Postgres: price + inventory query (index scan, warm) 8 ms
Recommendations service call (HTTP, same region) 25 ms
Template rendering + JSON serialization 12 ms
Compression (Brotli level 5, 120 KB → 30 KB) 4 ms
Response write (syscalls, kernel buffers) ~1 ms
Unaccounted (GC pauses, queueing, scheduler) 4 ms
------
60 msEach line maps to an earlier page. The Redis lookups are fast because they are one in-region round trip to data held in RAM. The Postgres query is fast because the B-tree's upper levels and the hot rows are in memory. A cold read that misses to SSD adds around 0.1 ms per page. A sequential scan of a large table can add seconds.
The "unaccounted" line is where GC pauses and thread-pool queueing live, and it is usually what grows at p99. The biggest item here is a downstream call. That is typical in microservice systems. If downstream calls run one after another instead of concurrently, their latencies add up.
Tip
Look for the largest single line first. Here, the recommendations call is 25 of 60 ms, so that is the place to start.
Common mistakes
- Reading the waterfall without checking for reuse. A missing DNS bar is good news, not an error.
- Trusting one trace. Compare several slow traces. Look for what they share.
- Ignoring gaps. Unexplained time between spans usually points to queueing, locks or GC.
Check yourself
- Why does a reused connection show no DNS or SSL bars in DevTools? Those phases were done on an earlier request and the connection is still open.
- In the breakdown above, which line should you attack first, and why? The recommendations call, at 25 ms, because it is the largest single item.
Key takeaways
- The browser waterfall splits a request into phases. Missing handshake bars mean connection reuse is working.
Server-Timingheaders put named server durations into the browser's Timing tab.- Distributed traces show each hop as a span. Gaps between spans point to queues, locks and GC.
- In the example, the 60 ms server time is mostly one downstream call, so that call is where to start.
Further learning
Next, use these tools to debug a slow endpoint hop by hop, in debugging a slow endpoint.
MDN: Server-Timing documents the header and its fields.
Related
Life of a request
Every step between typing a URL and seeing a page, with a latency budget for each hop, why the second request is faster, and a method for finding where a slow request loses its time.
Observability: logs and traces
Structured logs, distributed tracing and OpenTelemetry, so you can ask new questions of a running system instead of guessing which dashboard to open.
Performance and profiling
How to find where time goes before you optimize. Percentiles, two predictive laws, sampling profilers, off-CPU waiting and honest benchmarks, with a worked investigation of a slow endpoint. Start here for the performance toolkit.