# 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.

Source: https://howhttpworks.com/headers/performance-timing
Last reviewed: 2026-10-04

> **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 want | Use |
| --- | --- |
| Backend durations visible in DevTools and JavaScript | `Server-Timing` response header |
| Third-party scripts, images or API calls to expose detailed timings | `Timing-Allow-Origin` response header |
| Page load phases in JavaScript | `PerformanceNavigationTiming` |
| Per-request phases for subresources | `PerformanceResourceTiming` |
| Real-user vitals | `PerformanceObserver` for LCP, CLS, INP |

## Server-Timing

```http
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](https://howhttpworks.com/headers/server-timing).

Express example:

```javascript
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:

```http
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](https://howhttpworks.com/headers/timing-allow-origin).

## Navigation Timing in JavaScript

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

```javascript
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:

```javascript
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:

```javascript
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](https://howhttpworks.com/headers/cache-control)), [early hints](https://howhttpworks.com/headers/early-hints) (103), compression, and HTTP/2 or HTTP/3.

## Check it with curl

```bash
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)'
```

## Related

- [Server-Timing](https://howhttpworks.com/headers/server-timing), [Timing-Allow-Origin](https://howhttpworks.com/headers/timing-allow-origin), [X-Response-Time](https://howhttpworks.com/headers/x-response-time), [Early-Hints](https://howhttpworks.com/headers/early-hints)
