Developer Tools
Server-Timing Header Builder
Build and parse the Server-Timing HTTP header in your browser. Add metric name, dur, and desc, then copy ready snippets for Express, Nginx, Vercel, and more.
Server-Timing header builder
Quick start
Load a preset
Metric 1
Preview
Metric 2
Preview
Metric 3
Preview
Header value
Server-Timing
Single comma-joined header
Repeated headers (equivalent)
Metrics
3
non-empty names
Total dur
57.5 ms
sum of supplied durations
Errors
0
must fix before shipping
As it appears in Chrome DevTools
Server Timing column
One row per metric. Browsers display the description if present, otherwise the name. Rows without a dur are shown as a label only.
| Label | Time |
|---|---|
| Edge cache lookup | 0.5 ms |
| Postgres query | 45 ms |
| Template render | 12 ms |
Drop into your server
Server snippets
Express (Node.js)
// Express middleware
app.use((req, res, next) => {
res.setHeader("Server-Timing", "cache; dur=0.5; desc=\"Edge cache lookup\", db; dur=45; desc=\"Postgres query\", render; dur=12; desc=\"Template render\"");
next();
});Express, computed per request
// Express, computed per request
app.use((req, res, next) => {
const start = performance.now();
const phases = [];
phases.push({ name: "cache", dur: "0.5", desc: "Edge cache lookup" });
phases.push({ name: "db", dur: "45", desc: "Postgres query" });
phases.push({ name: "render", dur: "12", desc: "Template render" });
const header = phases
.map((p) => {
const parts = [p.name];
if (p.dur !== undefined) parts.push(`dur=${p.dur}`);
if (p.desc) parts.push(`desc="${String(p.desc).replace(/"/g, '\\"')}"`);
return parts.join("; ");
})
.join(", ");
res.setHeader("Server-Timing", header);
next();
});Apache (Header)
# .htaccess or vhost config Header always set Server-Timing "cache; dur=0.5; desc=\"Edge cache lookup\", db; dur=45; desc=\"Postgres query\", render; dur=12; desc=\"Template render\""
Nginx (add_header)
# server { ... } block
add_header Server-Timing "cache; dur=0.5; desc=\"Edge cache lookup\", db; dur=45; desc=\"Postgres query\", render; dur=12; desc=\"Template render\"" always;Vercel (vercel.json)
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Server-Timing",
"value": "cache; dur=0.5; desc=\"Edge cache lookup\", db; dur=45; desc=\"Postgres query\", render; dur=12; desc=\"Template render\""
}
]
}
]
}Netlify (_headers)
# _headers /* Server-Timing: cache; dur=0.5; desc="Edge cache lookup", db; dur=45; desc="Postgres query", render; dur=12; desc="Template render"
Cloudflare Workers
// Cloudflare Worker
export default {
async fetch(request) {
const response = await fetch(request);
const headers = new Headers(response.headers);
headers.append("Server-Timing", "cache; dur=0.5; desc=\"Edge cache lookup\", db; dur=45; desc=\"Postgres query\", render; dur=12; desc=\"Template render\"");
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
},
};Next.js (next.config.js)
// next.config.js
module.exports = {
async headers() {
return [
{
source: "/:path*",
headers: [
{ key: "Server-Timing", value: "cache; dur=0.5; desc=\"Edge cache lookup\", db; dur=45; desc=\"Postgres query\", render; dur=12; desc=\"Template render\"" },
],
},
];
},
};FastAPI (Python)
# FastAPI middleware
from starlette.middleware.base import BaseHTTPMiddleware
class ServerTimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
response = await call_next(request)
response.headers["Server-Timing"] = "cache; dur=0.5; desc=\"Edge cache lookup\", db; dur=45; desc=\"Postgres query\", render; dur=12; desc=\"Template render\""
return response
app.add_middleware(ServerTimingMiddleware)Rails (Ruby)
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
after_action :add_server_timing
private
def add_server_timing
response.set_header("Server-Timing", "cache; dur=0.5; desc=\"Edge cache lookup\", db; dur=45; desc=\"Postgres query\", render; dur=12; desc=\"Template render\"")
end
endGo net/http
// Go net/http middleware
func ServerTiming(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server-Timing", "cache; dur=0.5; desc=\"Edge cache lookup\", db; dur=45; desc=\"Postgres query\", render; dur=12; desc=\"Template render\"")
next.ServeHTTP(w, r)
})
}JavaScript (Performance API)
// In the browser, read Server-Timing for the current document
const nav = performance.getEntriesByType("navigation")[0];
if (nav && "serverTiming" in nav) {
for (const t of nav.serverTiming) {
console.log(t.name, t.duration, t.description);
}
}How browsers surface Server-Timing
- Chrome DevToolsshows the values in the Network panel under the Timing tab, beneath the browser's own metrics, in a row labeled Server Timing.
- Firefox DevTools shows them under the Timings tab when the response includes the header.
- PerformanceServerTiming entries are attached to every PerformanceResourceTiming entry on the same origin (or any origin that exposes the header via Timing-Allow-Origin).
- RUM agents (Datadog, New Relic, Sentry, Cloudflare Analytics) read the PerformanceResourceTiming entries, so backend phases land beside frontend metrics automatically once the header is set.
- For cross-origin reads, the response must include
Timing-Allow-Origin: *(or a specific origin) so the metrics are exposed to JavaScript.
Common pitfalls
- Putting units in dur. dur is always milliseconds. Do not append ms. Use
dur=12, notdur=12ms. - Spaces in the name. The name must be an RFC 7230 token. If you want a label with spaces, put the label in the
descparameter instead. - Forgetting to quote desc. A desc with whitespace or punctuation must be a double-quoted string. Some libraries always quote the desc; that is also valid.
- Repeating a metric name. DevTools and the Performance API key on the name. Use unique names per phase, or roll up phases with the same name on the server.
- Forgetting Timing-Allow-Origin. A cross-origin response will have the header in the network waterfall but the PerformanceServerTiming entry will be empty until the timing is exposed.
- Sending one repeated header per metric. Allowed by HTTP, but a single comma-separated header is smaller and renders the same way.
How to use
- Switch between Build a header and Parse an existing header at the top of the page. Load a preset (web request breakdown, API gateway, SSR, markers only, RUM) if you want a starting point.
- In Build mode, add one metric per row with a name (RFC 7230 token, like 'cache' or 'db'), an optional dur in milliseconds (12 or 12.5), and an optional desc (auto-quoted if it contains spaces). Use Up and Down to reorder rows and Remove to delete one.
- Copy the single comma-joined Server-Timing header value, the equivalent repeated headers, or any of the server snippets (Express, Apache, Nginx, Vercel, Netlify, Cloudflare Workers, Next.js, FastAPI, Rails, Go net/http) into your code or config.
- In Parse mode, paste any Server-Timing value (with or without the 'Server-Timing:' prefix; multiple lines are supported) and the tool splits it into rows, parses dur and desc, surfaces unknown parameters, and flags malformed entries.
- Review the Chrome DevTools preview table to confirm the metric labels and timings that will appear in the Network panel before you ship the header.
About this tool
Server-Timing Header Builder lets you compose, validate, and parse the Server-Timing HTTP response header defined by the W3C Server Timing recommendation. The header surfaces backend phase timings (edge cache lookup, database query, render, queue wait, upstream API, region) inside the browser's Network panel under the Server Timing column and inside the Performance API as PerformanceServerTiming entries that real user monitoring agents like Datadog, New Relic, Sentry, and Cloudflare Analytics read automatically. Each metric is rendered as 'metric-name; dur=<milliseconds>; desc="<human label>"', and multiple metrics are joined with commas as a single header or sent as repeated Server-Timing lines, both of which are semantically identical. The Builder mode adds rows for each metric where you type a name, an optional dur in milliseconds, and an optional desc; metric names are validated against the RFC 7230 token grammar (letters, digits, and a restricted punctuation set) so a space or comma inside the name is flagged before it ships, dur is validated as a non-negative number and pretty-printed without redundant zeros, and desc is auto-quoted when it contains whitespace with proper backslash escaping for embedded quotes and backslashes. Duplicate metric names are flagged because DevTools and the Performance API key on the name, very long dur values are surfaced as info, and control characters inside desc trigger an error. The output panel emits the single comma-joined header value, the equivalent repeated headers, a Chrome DevTools preview table that shows exactly how each row will render in the Server Timing column, a rollup of metric count and total dur, and ready-to-paste server snippets for Express, Express with computed per-request values, Apache (Header), Nginx (add_header), Vercel (vercel.json), Netlify (_headers), Cloudflare Workers, Next.js (next.config.js), FastAPI middleware, Rails after_action, and Go net/http middleware, plus a JavaScript snippet that reads Server-Timing from PerformanceNavigationTiming so frontend code can ingest the same metrics. The Parser mode takes any Server-Timing value (with or without the 'Server-Timing:' prefix, single line or many lines) and splits it on commas while respecting quoted descriptions, normalizes dur, surfaces unknown parameters (the spec only defines dur and desc; browsers ignore everything else), warns when a desc with whitespace is sent unquoted or when dur is non-numeric, and shows a cumulative dur rollup so you can spot which phase dominates the request. The page also explains pitfalls developers hit in practice: putting 'ms' inside dur, putting spaces inside the name instead of the desc, forgetting to quote a desc that contains whitespace, repeating a metric name and assuming both will render, and forgetting the Timing-Allow-Origin response header that exposes cross-origin Server-Timing entries to JavaScript. Everything runs locally in your browser; the metric names, durations, descriptions, and pasted header values never leave your device, which matters because backend timings often encode internal service names, region identifiers, feature flags, and tenant identifiers that should not be uploaded to a third party to validate.
Free to use. Works in your browser. No signup, no login.
Related tools
You may also like
Cache-Control Header Builder
Build and parse Cache-Control headers with directive flags, max-age presets, conflict checks, and ready-to-paste server snippets.
Open tool
DeveloperHTTP Link Header Builder
Build RFC 8288 Link headers with preload, canonical, hreflang, and parse existing values.
Open tool
DeveloperContent-Disposition Header Builder
Build RFC 6266 download headers with UTF-8 filenames; parse and explain existing values.
Open tool
DeveloperCSP Header Generator
Visual builder for the Content-Security-Policy HTTP header.
Open tool
DeveloperCORS Headers Generator
Build Access-Control headers with live validation and Apache, Nginx, Vercel, Netlify, Next.js, Worker, and Express snippets.
Open tool
DeveloperHTTP Headers Parser
Parse, classify, and decode HTTP headers, with a missing security headers audit.
Open tool