Server-Timing header: splitting TTFB into database, cache, and app time
Web Performance & Observability

The Server-Timing Header: Splitting TTFB into Database, Cache, and App Time

See your backend in the browser — the opaque Waiting bar, itemised.

Core Concept

The browser's network panel already splits a request into DNS lookup, connection, request sent, Waiting (TTFB), and content download. But everything the server did to produce the response — queries, cache lookups, business logic — is collapsed inside that single Waiting / TTFB bar. The Server-Timing response header lets the server itemise that opaque block, so the first byte you wait for becomes a readable breakdown of where the time actually went.

The Header, Exactly

Server-Timing: db;dur=45.1, cache;dur=2.3, app;dur=15.7 Server-Timing: db;desc="Postgres query";dur=45.1

Comma-separated phase metrics, each name;dur=milliseconds, with an optional desc="…" label for humans. DevTools renders them inside the request's timing view, and JavaScript can read them through PerformanceServerTiming on Resource/Navigation Timing entries.

Key Components

1 · Self-reported contract

Values are chosen by the server, freely. They may overlap, may run in parallel, and never include network transit or un-instrumented queueing. The header explains the waiting bar — it does not partition it.

2 · 63.1 ≠ TTFB

db 45.1 + cache 2.3 + app 15.7 = 63.1 ms. That sum is not required to equal TTFB. The rest is serialization, queueing, and anything the code never measured.

3 · JS access: PerformanceServerTiming

Cross-origin responses expose the header to JavaScript only when Timing-Allow-Origin permits it — though DevTools still displays the values.

4 · Information leak

Phase names and descriptions leak internal architecture (query engines, cache tiers, service names). Production deployments gate detail behind an internal-only condition.

How It Works

  1. Instrument server phases. Time the distinct stages of request handling — database, cache, application logic — with your own stopwatches.
  2. Emit the header. Attach Server-Timing: db;dur=45.1, cache;dur=2.3, app;dur=15.7 to the response. Only the server decides what to emit — an observability contract, not a measurement of truth; a slow phase can simply be left out.
  3. DevTools merges them in. The durations appear inside the request's timing view, extending the standard waterfall with server-side phases.
  4. Real-user monitoring collects both tiers. PerformanceServerTiming hands the server phases to JavaScript, so client phases (DNS, TTFB, download) and server phases (db, cache, app) travel together into your RUM data.

Falsify It

Instrument an endpoint whose emitted phases sum to far less than its measured TTFB. Then add a deliberate, un-instrumented sleep before the handler. DevTools' TTFB grows while every Server-Timing value stays constant — proving the header is reported, not derived. Then remove Timing-Allow-Origin on a cross-origin fetch: PerformanceServerTiming returns empty while the network panel still displays the values.

Real-World Applications

Latency attribution: db vs cache vs app RUM with backend phases Cache hit-rate observability (hit vs miss dur) SLO budgeting per request phase Staging vs production comparison

Checkpoint — answer before you move on

  1. The header reports db=45.1, cache=2.3, app=15.7, but the measured TTFB is 120 ms. Is the header "wrong"? What could the missing ~57 ms be? Think: what the header is allowed to exclude.
  2. Design the experiment that proves Server-Timing is self-reported rather than derived from the request. What stays constant, and what changes? An un-instrumented sleep is your instrument.
  3. A cross-origin fetch shows Server-Timing in the network panel, but PerformanceServerTiming returns empty in JavaScript. What is missing — and why does the panel still show the values? One header gates the JS API, not the panel.