Articles

The Server-Timing Header Explained

The Server-Timing HTTP header exposes backend performance metrics — database time, cache hits — directly in the browser's DevTools network panel.

Takina Takina · · 4 min read
Performance metrics displayed on a screen

The Server-Timing header is an HTTP response header that lets a backend report its own performance breakdown — how long a database query took, whether a cache was hit, how long a downstream API call blocked the response — directly to the browser. That data shows up in the DevTools Network panel’s Timing tab, right alongside the browser-side timing the client already measures, closing a long-standing blind spot: front-end performance tools can see how long a request took over the wire, but not what happened on the server while it waited.

What the header looks like

A response carries one Server-Timing header with one or more comma-separated metrics:

Server-Timing: db;dur=45.2, cache;desc="hit", auth;dur=12.1

Each entry has a short name, an optional dur (duration in milliseconds), and an optional desc (a human-readable description). Multiple metrics can also be split across repeated headers, which some server frameworks find easier to emit incrementally:

Server-Timing: db;dur=45.2
Server-Timing: cache;desc="hit"

Setting it from your backend

In a Node.js server, this is just another response header:

app.get("/api/orders", async (req, res) => {
  const dbStart = performance.now();
  const orders = await db.query("SELECT * FROM orders WHERE user_id = $1", [req.userId]);
  const dbDur = performance.now() - dbStart;

  res.setHeader("Server-Timing", `db;dur=${dbDur.toFixed(1)}`);
  res.json(orders);
});

For multiple stages, build up the header value as you go:

const timings = [];

function mark(name, durationMs) {
  timings.push(`${name};dur=${durationMs.toFixed(1)}`);
}

// ... mark("db", dbDur); mark("cache-lookup", cacheDur); ...

res.setHeader("Server-Timing", timings.join(", "));

The values are visible to anyone who opens DevTools, so avoid putting anything sensitive in the desc field — internal hostnames, query text, or user data have no place in a header meant for performance debugging.

Reading it in DevTools

Open the Network panel, select a request, and go to the Timing tab. Below the browser’s own phases (queuing, DNS, connect, TLS, request sent, waiting, content download), each Server-Timing entry appears as its own labeled segment. This is the piece that’s normally invisible from the client: the “waiting” phase in a browser’s timing view is a single opaque bar, and Server-Timing is what breaks it open into “45ms in the database, 12ms in auth, the rest in application logic.”

Why this matters for diagnosing slow requests

Without Server-Timing, a slow API response looks identical from the browser whether the bottleneck is a missing database index, a cold cache, an overloaded downstream service, or serialization of a large payload — all of it collapses into one number. That ambiguity is exactly what makes Time to First Byte hard to act on: a high TTFB tells you the server was slow, not why.

Server-Timing turns that single number into a breakdown you can act on directly, and it’s the natural complement to HTTP caching headers: a cache;desc="miss" entry tells you immediately whether a slow response was a cache miss or a genuinely slow computation, without cross-referencing server logs.

Aggregating it beyond DevTools

DevTools is useful for one request at a time, but Server-Timing values are also exposed to JavaScript through the Resource Timing and Navigation Timing APIs, via PerformanceResourceTiming.serverTiming:

const [entry] = performance.getEntriesByName("/api/orders", "resource");
for (const { name, duration, description } of entry.serverTiming) {
  console.log(name, duration, description);
}

This is how real-user-monitoring tools turn Server-Timing into aggregate dashboards — sampling actual user requests in production rather than relying on synthetic tests, and correlating backend timing with the Core Web Vitals those same requests contributed to.

A word on overhead and precision

Timing your own code with performance.now() calls adds negligible overhead — microseconds against a response that’s already taking milliseconds — but it’s still worth being deliberate about which stages you instrument. Wrapping every function call in a timing mark produces a header so cluttered it defeats the purpose; the useful granularity is usually the handful of stages that plausibly explain most of a slow response: the database, an external API call, serialization, and any cache lookups. That mirrors how you’d approach the critical rendering path on the client side — measure the phases that matter, not every function in the call stack.

Server-Timing vs client-side timing APIs

Client-side (Navigation/Resource Timing)Server-Timing
MeasuresNetwork and browser phasesBackend processing phases
Set byBrowser automaticallyApplication code, per response
Visible inDevTools Timing tab, performance APIDevTools Timing tab, performance API
GranularityDNS, connect, TLS, request, responseWhatever the backend chooses to report
Requires code changesNoYes, on the server

The two are complementary rather than competing: client-side timing tells you where time went across the network, and Server-Timing tells you where it went once the request reached your infrastructure.

The takeaway

The Server-Timing header is a small addition to a response — a comma-separated list of named durations — that pays off every time someone has to debug a slow endpoint. It turns an opaque “waiting” phase in DevTools into a labeled breakdown of database time, cache status, and downstream calls, without adding logging infrastructure or a tracing system. Instrument the handful of stages that plausibly explain a slow response, keep the descriptions free of sensitive detail, and the next slow-request investigation starts in the Network panel instead of the server logs.

The Lycoris Team The Lycoris Team · · 5 min read

Head-of-Line Blocking Explained: HTTP/1.1, HTTP/2, QUIC

Head-of-line blocking is when one stalled item holds up everything queued behind it. How it affects HTTP/1.1, HTTP/2 over TCP, and how QUIC fixes it.

#Networking #Web Development #Performance
Takina Takina · · 4 min read

What Is WebTransport? A Guide to the API

WebTransport is a browser API for low-latency, bidirectional client-server messaging over HTTP/3. How it works and when to reach for it.

#Web Development #Networking #Performance
Takina Takina · · 4 min read

HTTP Range Requests and Partial Content, Explained

HTTP range requests let a client ask for just part of a resource, enabling video seeking, resumable downloads, and partial file fetches over HTTP.

#Networking #Web Development #Performance