Age header (tested)
Age says how many seconds a response has already sat in a cache. Browsers subtract it from max-age. We measured real CDNs and Chromium to show what that means in practice.
1. What Age means
RFC 9111 (section 5.1) defines Age as the sender's estimate of the time since the response was generated or last validated at the origin. The value is whole seconds. A cache adds it when it serves a stored response.
The rule that matters: a response is fresh while its current age is less than its freshness lifetime. In plain terms, freshness left = max-age − Age.
A response fetched straight from the origin usually has no Age header, or Age: 0. The spec says a cache that sends a just-fetched response may send Age: 0.
2. Real CDNs: two curl runs 5 seconds apart
curl -sI https://code.jquery.com/jquery-3.7.1.min.js | grep -iE '^(age|cache-control|x-cache)'
sleep 5
curl -sI https://code.jquery.com/jquery-3.7.1.min.js | grep -iE '^(age|cache-control|x-cache)'
This is what we got:
| URL | Cache-Control | Age run 1 | Age run 2 | X-Cache |
|---|---|---|---|---|
| www.wikipedia.org/ | s-maxage=86400, must-revalidate, max-age=3600 | 18700 | 18706 | cp3069 miss, cp3069 hit |
| code.jquery.com jquery-3.7.1.min.js | public, max-age=31536000, stale-while-revalidate=604800 | 4122607 | 4122613 | HIT, HIT |
| cdn.jsdelivr.net react@18/package.json | public, max-age=604800, s-maxage=43200 | 33152 | 33157 | HIT, HIT |
| developer.mozilla.org/ | max-age=3600,public | 1329 | 1334 | MISS, HIT, HIT |
| www.bbc.co.uk/ | private, ..., max-age=0, must-revalidate | (none) | (none) | (none) |
Age went up by 5 or 6 seconds between runs, as expected. The same cached copy was served both times. None of the five sent CF-Cache-Status; the X-Cache and X-Served-By: cache-... headers on jQuery, jsDelivr and MDN are the Fastly style. jQuery and MDN also showed Via: 1.1 varnish. BBC sends private with max-age=0, so no shared cache stores it and there is no Age.
3. Do the subtraction
| URL | max-age | Age | Freshness left in your browser |
|---|---|---|---|
| jquery-3.7.1.min.js | 31536000 | 4122607 | 27413393 s (about 317 days) |
| react@18/package.json | 604800 | 33152 | 571648 s (about 6.6 days) |
| developer.mozilla.org/ | 3600 | 1329 | 2271 s (about 38 min) |
| www.wikipedia.org/ | 3600 | 18700 | none: already stale |
Wikipedia is the interesting row. The CDN keeps the page for s-maxage=86400 (a day), but the browser uses max-age=3600. Age 18700 is more than 3600, so the browser gets a response that is already stale. With must-revalidate it has to check back next time. For jsDelivr the CDN's own limit is s-maxage=43200, so the CDN copy has 43200 − 33152 = 10048 s left.
4. Chromium test: Age eats into max-age
We ran a tiny Node.js server on port 8986. Every response had Cache-Control: max-age=60. Three URLs sent different Age values. The server counted each request. Chromium fetched all three at t=0, t=5 s and t=12 s.
res.writeHead(200, {'cache-control': 'max-age=60', 'age': age});
// page: fetch('/a50'), fetch('/a100'), fetch('/a0') at t=0, 5, 12 s
| Headers | Freshness left | t=0 | t=5 s | t=12 s | Server hits |
|---|---|---|---|---|---|
| max-age=60, Age: 0 | 60 s | server | cache | cache | 1 |
| max-age=60, Age: 50 | 10 s | server | cache | server | 2 |
| max-age=60, Age: 100 | none | server | server | server | 3 |
With Age: 50 the copy stayed fresh at 5 s and expired before 12 s, so Chromium went back to the server. With Age: 100 the response was stale on arrival and Chromium never served it from cache. With Age: 0 one request covered all three fetches.
5. Age is set by caches, not your origin
Caches such as Varnish, Fastly and Cloudflare add or update Age when they serve a stored copy. Per their docs, Varnish sets Age from the time the object has been in its cache. Fastly, which is built on Varnish, does the same and adds the age it received from upstream. Cloudflare sends Age on cache hits.
If your origin sends an Age header itself, caches and browsers will trust it. An origin that sends Age: 3600 with max-age=3600 makes its own pages uncacheable, as our Age: 100 test shows. This often happens by accident when one cache sits behind another, or when a framework copies headers from an upstream response. Check for it and remove it at the origin.
When several caches are stacked, each one adds its own time. That is why the Age you see at the edge can be far larger than how long the edge itself held the copy. The Via header shows how many hops there were.
6. Quick checklist
- See an Age value? A cache served this response. Compare it to
max-age(browser) ors-maxage(CDN). - If Age is greater than max-age, browsers treat the response as stale. Use stale-while-revalidate or a bigger max-age if that is not what you want.
- Stale is not the same as re-downloaded: with an ETag or Last-Modified, the browser can get a 304 Not Modified.
- No Age on a page marked
privateis normal. See private vs public.
Frequently asked questions
What does the Age header mean?
It is the number of seconds since the response was generated or last validated at the origin. Caches add it. In our test, Age on five-second-apart requests to jQuery's CDN went from 4122607 to 4122613.
Does the browser subtract Age from max-age?
Yes. In Chromium, max-age=60 with Age: 50 stayed cached for about 10 seconds; at 12 seconds Chromium fetched from the server again.
What happens if Age is bigger than max-age?
The response is stale on arrival. With max-age=60 and Age: 100, Chromium hit our server on all three fetches and never used the cache.
Should my origin server send an Age header?
No. Age is for caches to set. If your origin sends it, caches and browsers count it against max-age and can stop caching your pages.
Why is Age missing on some sites?
Either no shared cache stored the response (for example BBC's homepage is private with max-age=0) or the response came straight from the origin. A fresh origin fetch may also show Age: 0.
Written with AI assistance. Measured on 2026-10-02 with curl 8.14.1 against Wikipedia, code.jquery.com, jsDelivr, BBC and MDN (two runs about 5 seconds apart), and with headless Chromium 153 (Playwright) plus a Node.js 24.21.0 test server that counted every request. PlainHeaders is our extension; curl, Node.js and Chromium are free.