How HTTP Works

Performance-Timing: Server-Timing, Timing-Allow-Origin and Performance APIs

There is no Performance-Timing HTTP header. Use Server-Timing, Timing-Allow-Origin, the Navigation and Resource Timing APIs, TTFB and INP instead.

Reviewed 3 min readadvanced6 sourcesTry itMarkdown
Direction
Response
Category
Diagnostics
Status
Not a real header: Not a registered header; use Server-Timing
On this page

TL;DR: There is no Performance-Timing HTTP header. To report backend durations send Server-Timing, to let other origins read timings send Timing-Allow-Origin, and to measure in the browser use PerformanceNavigationTiming and PerformanceObserver (Core Web Vitals now use INP, not FID).

This URL is kept because people search for it. The name usually comes from one of three things: a custom header invented by an in-house proxy, the old window.performance.timing JavaScript object, or confusion between the Performance APIs and HTTP headers. Here is what each one maps to.

What to use instead

You wantUse
Backend durations visible in DevTools and JavaScriptServer-Timing response header
Third-party scripts, images or API calls to expose detailed timingsTiming-Allow-Origin response header
Page load phases in JavaScriptPerformanceNavigationTiming
Per-request phases for subresourcesPerformanceResourceTiming
Real-user vitalsPerformanceObserver for LCP, CLS, INP

Server-Timing

HTTP/1.1 200 OK
Server-Timing: db;dur=47.2;desc="Postgres", cache;desc="miss", app;dur=12.1, total;dur=83.5

Each metric has a name and optional dur (milliseconds) and desc. Chrome DevTools shows them in the Network panel under Timing, and JavaScript reads them from entry.serverTiming. Use short names, and consider omitting the header in production for unauthenticated users, because it exposes backend structure. CDNs commonly add their own entries (for example Cloudflare’s cfCacheStatus, or cdn-cache; desc=HIT on Fastly). Details on Server-Timing.

Express example:

app.use((req, res, next) => {
  const start = process.hrtime.bigint()
  const onHeaders = require('on-headers')
  onHeaders(res, () => {
    const ms = Number(process.hrtime.bigint() - start) / 1e6
    res.setHeader('Server-Timing', `app;dur=${ms.toFixed(1)}`)
  })
  next()
})

nginx can expose upstream time with add_header Server-Timing "upstream;dur=$upstream_response_time" always;, but this value is in seconds, not milliseconds, so convert it or rename the metric.

Timing-Allow-Origin

For cross-origin resources, browsers hide most Resource Timing details (DNS, connect, request and response start, sizes) unless the response allows it:

Timing-Allow-Origin: https://www.example.com

Timing-Allow-Origin: * exposes timings to any page and is common on CDN assets. It is independent of CORS: it does not let pages read the body. See Timing-Allow-Origin.

performance.timing (Level 1) is deprecated. Use the Level 2 entry:

const [nav] = performance.getEntriesByType('navigation')

const ttfb = nav.responseStart // ms since navigation start
const serverTime = nav.responseStart - nav.requestStart
const dns = nav.domainLookupEnd - nav.domainLookupStart
const tcpTls = nav.connectEnd - nav.connectStart
const download = nav.responseEnd - nav.responseStart
console.log({ ttfb, serverTime, dns, tcpTls, download, protocol: nav.nextHopProtocol })
console.log(nav.serverTiming) // parsed Server-Timing entries

Resource Timing for subresources:

performance.getEntriesByType('resource')
  .filter((e) => e.transferSize === 0 && e.decodedBodySize > 0) // from cache, or TAO missing
  .forEach((e) => console.log(e.name, e.duration))

A transferSize of 0 on a cross-origin entry means either a cache hit or a missing Timing-Allow-Origin.

Core Web Vitals

Observe vitals with PerformanceObserver, or use the web-vitals library:

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) console.log('LCP', entry.startTime, entry.element)
}).observe({ type: 'largest-contentful-paint', buffered: true })

The current Core Web Vitals are LCP (loading, good at 2.5 s or less), INP (responsiveness, good at 200 ms or less) and CLS (visual stability, good at 0.1 or less). INP replaced FID in March 2024. TTFB is a diagnostic, not a Core Web Vital, but a slow TTFB caps your LCP. HTTP-level fixes for it: caching (Cache-Control), early hints (103), compression, and HTTP/2 or HTTP/3.

Check it with curl

curl -s -o /dev/null -w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' https://example.com/
curl -sI https://example.com/ | grep -iE '^(server-timing|timing-allow-origin)'

Frequently asked questions

Is there a Performance-Timing HTTP header?

No. It is not defined by any IETF, W3C or WHATWG specification and is not registered with IANA. Some in-house systems use the name for custom durations, but browsers do nothing with it. The standard header for backend durations is Server-Timing.

What is performance.timing and is it deprecated?

performance.timing is the Navigation Timing Level 1 JavaScript object, not an HTTP header. It is deprecated and replaced by PerformanceNavigationTiming, available through performance.getEntriesByType("navigation"), which uses high-resolution timestamps relative to the navigation start.

How do I measure TTFB in the browser?

Read the navigation entry: performance.getEntriesByType("navigation")[0].responseStart is the time from navigation start to the first byte of the response. It includes redirects, DNS, TCP/TLS and server think time; subtract requestStart for server time alone.

Why is FID no longer a Core Web Vital?

Interaction to Next Paint (INP) replaced First Input Delay as a Core Web Vital in March 2024. INP measures the latency of all interactions across the visit, not just input delay on the first one. A good INP is 200 ms or less at the 75th percentile.

Why are cross-origin resource timings zero?

Detailed Resource Timing fields such as transferSize and the DNS and connect phases are zeroed for cross-origin responses unless they send Timing-Allow-Origin. Add the header on the third-party resource, or on your CDN, to expose them.

Can the browser read my Server-Timing values?

Yes, via the serverTiming property of navigation and resource entries. For cross-origin resources the values are only exposed when Timing-Allow-Origin allows the page. The values are visible to every user, so do not put secrets or exact internal topology in them.

Sources

  1. MDN Web Docs: Performance APIdeveloper.mozilla.org
  2. MDN Web Docs: Server-Timingdeveloper.mozilla.org
  3. W3C Server Timingw3.org
  4. W3C Navigation Timing Level 2w3.org
  5. W3C Resource Timingw3.org
  6. web.dev: Interaction to Next Paint (INP)web.dev

Keep going

Browse /search