LobeLOBE

Learn

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.

Measure it instead of guessing: Lobe is a free, local HTTP profiler — one command captures every request’s DNS, TCP, TLS, TTFB, and download time, judged against grounded baselines. No agent, no account. brew install kpwithcode/lobe/lobe or cargo install lobe-cli. How it works →