Server-Timing header examples (Express, Django, Rails, Next.js)
Server-Timing is a standard HTTP response header that lets your app report where server time went — Server-Timing: db;dur=190, render;dur=40 — so any client, browser DevTools, or profiler can split time-to-first-byte into named segments. It’s one middleware line in any framework, adds a few dozen bytes per response, and needs no agent or SDK. Copy-paste examples below.
The header format
Server-Timing: db;dur=190, render;dur=40, cache;desc="hit" name required your label for the segment (db, render, auth, ...) dur= optional duration in milliseconds desc= optional human-readable note (e.g. cache "hit" vs "miss")
Express (Node.js)
app.get("/api/users", async (req, res) => {
const t0 = performance.now();
const users = await db.query("SELECT ...");
const dbMs = performance.now() - t0;
const t1 = performance.now();
const body = render(users);
const renderMs = performance.now() - t1;
res.setHeader(
"Server-Timing",
`db;dur=${dbMs.toFixed(1)}, render;dur=${renderMs.toFixed(1)}`
);
res.json(body);
});Django (Python)
# middleware.py
import time
class ServerTimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
start = time.perf_counter()
response = self.get_response(request)
total_ms = (time.perf_counter() - start) * 1000
response["Server-Timing"] = f"view;dur={total_ms:.1f}"
return response
# For per-query timing, add django-debug-toolbar's numbers or wrap
# connection.queries in the view and emit db;dur= alongside view;dur=.Rails (Ruby)
Rails ships it for free: ActionDispatch::ServerTiming is enabled by default in development since Rails 7, emitting segments for database time, view rendering, and more. For production, add the middleware explicitly:
# config/environments/production.rb config.middleware.use ActionDispatch::ServerTiming
Next.js (route handlers)
export async function GET() {
const t0 = performance.now();
const data = await fetchFromDb();
const dbMs = performance.now() - t0;
return Response.json(data, {
headers: { "Server-Timing": `db;dur=${dbMs.toFixed(1)}` },
});
}Where the numbers show up
Browser DevTools renders the segments in the Network panel’s Timing tab. Local profilers that capture your real traffic can do more with it: Lobe, for example, reads the header on every proxied request and splits the TTFB bar into your named segments — so “280ms TTFB” becomes “190ms db, 40ms render, 50ms unattributed” without any further instrumentation.
Two cautions: the header is visible to anyone who inspects the response, so keep labels generic in production (db, not table names) — and treat reported durations as self-declared: they describe your server’s opinion of where time went, inside the externally measured TTFB.