Timing-Allow-Origin Header: Unmask Zero-Duration Cross-Origin Resource Timing in 2026
Cross-origin resources report zero for duration, transferSize, and every phase timing unless the server emits Timing-Allow-Origin. Set it correctly on CDN, S3, or your API to unlock full Resource Timing and honest RUM data.
The Timing-Allow-Origin (TAO) response header tells the browser it's safe to expose detailed timing data (duration, transferSize, DNS/TCP/TLS phases, nextHopProtocol, and any Server-Timing values) for a cross-origin resource to the page that loaded it. Without TAO, PerformanceResourceTiming entries for cross-origin fetches return 0 for every one of those fields. That's exactly why your RUM dashboard shows suspiciously fast third-party scripts, and why "TTFB attribution" on a CDN-hosted asset is a black box. Setting Timing-Allow-Origin: * (or a specific origin) on the CDN, bucket, or third-party endpoint fixes it.
Cross-origin resources without Timing-Allow-Origin report 0 for duration, transferSize, encodedBodySize, decodedBodySize, and every phase timing (DNS, TCP, TLS, request, response).
TAO is not the same as CORS. A CORS-enabled resource is still opaque to Resource Timing unless it also emits Timing-Allow-Origin.
Server-Timing values are gated by TAO too. No header, no server metrics in the browser, even from your own subdomain.
S3, CloudFront, Cloudflare, and Fastly all require explicit configuration; none send TAO by default in 2026.
The safest values are Timing-Allow-Origin: * for public assets and Timing-Allow-Origin: https://your-site.example for internal APIs.
You can audit missing TAO in one line of DevTools: performance.getEntriesByType('resource').filter(e => e.transferSize === 0 && e.decodedBodySize === 0).
What is Timing-Allow-Origin?
Timing-Allow-Origin is an HTTP response header defined in the Resource Timing Level 3 specification. It's a per-resource opt-in that basically says "this document loaded me from a different origin, but I trust you enough to see how long I took." When the browser receives a resource response, it checks the response's origin against the document's origin. Same origin? Full timing is exposed. Different origin? The browser looks for Timing-Allow-Origin. If the header is missing, or its value doesn't match the document's origin (or *), the browser strips out anything that could be used as a side-channel to probe a foreign server.
The header takes a comma-separated list of origins or a wildcard:
There is no such thing as Timing-Allow-Origin: null, and there is no credentials mode. TAO is a much simpler opt-in than CORS. Either the target origin lets you see its timings, or it doesn't.
Why is Resource Timing 0 for cross-origin resources?
Because timing data leaks information. If a browser exposed the exact duration of a request to bank.example without permission, a malicious page could measure how long bank.example/user?id=42 takes and infer whether that user exists based on response-time differences. That's a classic timing side-channel, and browsers have been closing them off for years. The blanket rule in the spec: any timing that could reveal something about a foreign server's internals is set to 0 unless that server explicitly opts in with TAO.
Concretely, here's what you see in DevTools for a TAO-missing resource:
Only startTime, responseEnd, and the resource URL survive. That's why so many RUM dashboards report a suspiciously flat "third-party latency" of 0 ms. The numbers aren't wrong; the browser is just refusing to hand them over.
Is Timing-Allow-Origin the same as CORS?
No. This trips up almost everyone I've onboarded to RUM. CORS (Access-Control-Allow-Origin) governs whether JavaScript can read the response body. TAO governs whether JavaScript can read the timing entries. They are independent; one does not imply the other.
Dimension
CORS (Access-Control-Allow-Origin)
Timing-Allow-Origin
Governs
Reading response body from JS
Reading Resource Timing fields
Applies to
fetch(), XMLHttpRequest, module scripts
All subresources (img, script, css, font, xhr)
Credentials mode
Yes (Access-Control-Allow-Credentials)
None
Preflight
Sometimes (OPTIONS)
Never
Default when missing
Response body blocked from JS
Timings zeroed
Wildcard allowed
Yes (but not with credentials)
Yes (always)
Header on OPTIONS?
Required for preflight
Not applicable
A resource can have full CORS and no TAO. You can fetch() and read its JSON just fine, but performance.getEntriesByType('resource') will still return 0 for its transfer size. That's the most common trap: your API sets Access-Control-Allow-Origin: * so your SPA works, then you wire up RUM and every API call shows transferSize: 0. The API also needs Timing-Allow-Origin. I hit this exact bug shipping a dashboard last year, and it took me an embarrassing hour of staring at the network panel before the penny dropped.
What fields does TAO unlock?
Setting a valid Timing-Allow-Origin header exposes these fields on the resource's PerformanceResourceTiming entry:
Transfer sizes: transferSize (bytes over the wire including headers), encodedBodySize (compressed body), decodedBodySize (uncompressed body). These three together tell you compression ratio and whether the resource came from cache. If transferSize === 0 && decodedBodySize > 0, that's a memory or disk cache hit.
Network phases: domainLookupStart/End (DNS), connectStart/End (TCP), secureConnectionStart (TLS handshake), requestStart (first byte out), responseStart (first byte in, which is the TTFB for that resource).
Protocol: nextHopProtocol, showing "h3", "h2", or "http/1.1". Essential for confirming your CDN actually served the request over HTTP/3.
Server-Timing: serverTiming[], containing every Server-Timing entry your origin emitted. This is how our Server-Timing header TTFB attribution approach actually reaches the browser for third-party subresources.
Redirect timing: redirectStart, redirectEnd. Useful for detecting sneaky 301 chains from ad partners.
Worker timing: workerStart, populated when a Service Worker intercepted the fetch. Critical when debugging Service Worker Static Routing API configurations.
All of these are essential for meaningful RUM. Without TAO, you can basically only tell that a resource loaded and when it finished. Nothing about what happened in between.
How do you set Timing-Allow-Origin?
TAO is a response header. You set it wherever your response headers get set: origin server, CDN, or object storage bucket. The mechanics differ by platform, but the value is always the same shape.
None of the big CDNs or object stores emit Timing-Allow-Origin by default in 2026. Every one of them requires you to opt in, usually through a response-header policy or an edge function.
Set Override: true so CloudFront replaces any (empty) value forwarded from S3. Apply the policy to the default cache behaviour, then invalidate /* to flush cached responses that lack the header.
Cloudflare
Cloudflare exposes TAO through Transform Rules > HTTP Response Header Modification. Create a rule with the expression true (match all responses) and add:
Set static: Timing-Allow-Origin = *
Alternatively, use a Worker as shown above if you need conditional logic (e.g., restrict TAO to your own origin in production but wildcard in staging).
Fastly (VCL)
sub vcl_deliver {
set resp.http.Timing-Allow-Origin = "*";
}
Google Cloud Storage
Unlike CORS, GCS does not expose a native TAO configuration. Front the bucket with a Cloud CDN or Cloud Run service that appends the header, or serve assets through a Cloudflare/Fastly layer.
Audit missing TAO in DevTools and RUM
Open any page in Chrome, then paste this into the DevTools console:
This produces the punch list of URLs whose owners haven't set TAO. Every row is a resource your RUM is measuring inaccurately. Filter by initiatorType to prioritise: script initiators are where INP damage lives, img initiators are where LCP damage lives, and fetch/xmlhttprequest initiators are your API calls.
A more thorough audit script:
function auditTAO() {
const entries = performance.getEntriesByType('resource');
const byOrigin = new Map();
for (const e of entries) {
const origin = new URL(e.name).origin;
if (origin === location.origin) continue;
const blocked = e.transferSize === 0 && e.decodedBodySize === 0;
const bucket = byOrigin.get(origin) || { total: 0, blocked: 0 };
bucket.total++;
if (blocked) bucket.blocked++;
byOrigin.set(origin, bucket);
}
console.table(
[...byOrigin.entries()].map(([origin, { total, blocked }]) => ({
origin,
total,
blocked,
coverage: `${Math.round((1 - blocked / total) * 100)}%`
}))
);
}
auditTAO();
Run this on production and you'll almost certainly see a handful of vendor origins at 0% coverage. Those are the ones to escalate. Internal ones you can fix at the CDN, third-party ones need a ticket.
TAO and Server-Timing attribution
The Server-Timing header is one of the highest-value tools for TTFB attribution. You can emit metrics like Server-Timing: db;dur=42, cache;dur=1, render;dur=13 from the origin, and they surface in PerformanceResourceTiming.serverTiming in the browser. But that array is only populated for cross-origin resources when TAO is present. This is the single most common reason engineers add Server-Timing to their CDN or third-party origin, see nothing in the browser, and conclude "Server-Timing doesn't work here." It works. TAO is just missing.
Without the last line, the server-timing line above is discarded from the browser's view. Pair TAO with our Cache-Status header for CDN debugging approach and you get a complete picture of every hop from browser to origin, per resource, in production.
Third-party scripts that refuse TAO
Not every vendor sets Timing-Allow-Origin: *, and some are pointedly ignorant of it. In my last audit (a large e-commerce catalogue), the offenders were:
Some analytics providers: their CDN endpoints for beacon collection often omit TAO. Occasionally, if you ask, they'll enable it per-customer, but you have to escalate through support.
Chat widgets: the initial loader often has TAO; the WebSocket bootstrapper and follow-on JS chunks frequently don't.
Ad networks: nearly 100% do not set TAO, and never will, because they don't want you to know how much they cost you.
Legacy CMS plugins hosted on shared infrastructure: the vendor doesn't own the CDN config and can't add the header.
Practical mitigation: for scripts you cannot force TAO on, self-host or proxy them through your own CDN and add the header on the way out. This has the additional benefit of putting the request on the same connection (HTTP/2 or HTTP/3 to your origin) instead of opening a fresh TLS handshake to tracker.example. It's not always contractually allowed (check the vendor's TOS), but for the ones where it is, expect a measurable LCP improvement plus honest RUM data. See our guide on third-party script facades for the deferred-loading pattern that pairs well with proxying.
Security considerations
Setting Timing-Allow-Origin: * on a resource exposes precise network-level timings to any page that loads it. For public static assets (JS, CSS, fonts, images on a CDN) that's fine. For an authenticated API endpoint, ask whether a timing side-channel is meaningful in your threat model. If a login endpoint returns in 100 ms for valid usernames and 200 ms for invalid ones, TAO makes that difference measurable from any page on the internet. The exposure is small (attackers can already time these via fetch() in most CORS-open configurations), but if your security team is strict, prefer a specific-origin allowlist:
Also read the MDN Timing-Allow-Origin reference for the precise matching rules. The origin comparison is case-sensitive scheme+host+port; no wildcards inside the value, only the standalone *.
Frequently Asked Questions
Does Timing-Allow-Origin require CORS to be enabled?
No. TAO is completely independent of CORS. A resource can opt into TAO without ever setting Access-Control-Allow-Origin, and vice versa. The two headers are checked independently by the browser.
Is Timing-Allow-Origin: * safe to set on all responses?
For static public assets, yes. The timings expose nothing sensitive. For authenticated endpoints where response time correlates with secret state (e.g., "does this username exist"), consider a specific-origin allowlist instead of the wildcard.
Does Timing-Allow-Origin apply to same-origin resources?
No. Same-origin resources always expose full timing, and the header is ignored. TAO only matters when the resource origin differs from the document origin.
Why does my resource show transferSize: 0 even after adding TAO?
Three common causes: (1) the CDN cached the old response before TAO was added, so invalidate the cache; (2) the header is added by a rule that only fires on 2xx responses, but the resource is a 304 Not Modified, so ensure your rule covers all status codes; (3) a downstream proxy is stripping unknown headers.
Do Server-Timing values require Timing-Allow-Origin for cross-origin resources?
Yes. Without TAO, the serverTiming array on PerformanceResourceTiming is empty for any cross-origin resource, regardless of what Server-Timing values the origin emitted. TAO is the gate.
Can a Service Worker add Timing-Allow-Origin to a response?
A Service Worker can synthesise a response with any headers it wants, and if the page reads that Response via Resource Timing it will see the timings. But this only helps for resources the Service Worker itself constructs. You cannot use a SW to add TAO to a real cross-origin fetch and unblock the true network timings.
The NotRestoredReasons API is how you learn, per real user navigation, why Chrome refused to restore your page from bfcache. Here's the RUM snippet I ship in production, the full 2026 blocker list, and how to fix each one.
Learn how to optimize First Contentful Paint below 1.8s at p75. Covers TTFB reduction, render-blocking CSS, web fonts, 103 Early Hints, and Speculation Rules prerender with production code.
Understand every value of the CSS contain property (layout, paint, size, strict, content) with 2026 browser support notes, DevTools workflow, and real-world INP wins.