How HTTP Works

Response header

< Age: 120

Age Header

Learn how the Age header indicates how long a response has been cached in seconds. Understand cache freshness calculations and CDN behavior.

Reviewed 6 min readintermediate5 sourcesTry itMarkdown
Direction
Response
Category
Caching
Spec
RFC 9111 §5.1
On this page

TL;DR: Age tells you how many seconds ago the origin generated or last validated a response. It counts time spent in every cache along the way plus time in transit, not just the cache closest to you. To tell a hit from a miss, read the cache status header; Age: 0 on its own can’t answer that.

What is Age?

When a cache serves a stored response without checking with the origin, it has to add an Age header, overwriting any value already there with its own calculated current age. The definition is in RFC 9111 §5.1.

Age is about the HTTP response, not the data inside it. An API can build a brand-new response from a stale database row, and that response’s Age will still be zero.

Syntax

Headers and outputs below are examples.

Age: 24

The value is a whole number of seconds, zero or more. It’s not a date, and it’s not a TTL.

How Age Works

Age accumulates. When an edge cache gets a response from an upstream cache, it starts from the age that upstream already reported. Copying an object from one cache to another doesn’t make it new again. Revalidating with the origin, on the other hand, resets the baseline, even if the body comes back identical.

Age is also a poor hit/miss signal. A missing Age field doesn’t mean the origin handled the request, and Age: 0 looks the same whether it came from a cache that just stored the response or from anything else with a near-zero age. For hit/miss, use your CDN’s status header or its logs.

Age Calculation

RFC 9111 §4.2.3 combines four inputs: the origin’s Date, the Age received from upstream, the request/response delay, and how long the response has sat in the local cache. The conservative version looks like this:

apparent = max(0, response_received_at - origin_date)
from_upstream = received_age + (response_received_at - request_sent_at)
initial = max(apparent, from_upstream)
current = initial + (now - response_received_at)

Everything is in seconds. This is why now - time_cached isn’t enough on its own. The RFC also lets a cache use the corrected upstream age directly when every cache in the chain does proper age accounting.

A worked example, with all clock readings in seconds on one timeline:

origin Date:             1000
request sent:            1005
response received:       1007
upstream Age:              12
now:                     1010

apparent age:               7
corrected upstream age:    14
initial age:               14
current age:               17

This cache has held the response for three seconds, but its current age is seventeen. If it only counted local residence, you’d lose the twelve seconds reported upstream and the two-second request delay. If the origin’s Date is in the future, apparent age clamps to zero rather than going negative, and the upstream age still counts. When you’re chasing clock skew, keep the raw Date and Age values side by side instead of treating either one as a stopwatch.

Freshness Calculation

A response is fresh while its freshness lifetime is greater than its current age. Once they’re equal, it’s stale. Shared caches use s-maxage over max-age. If neither is set, Expires or a permitted heuristic supplies the lifetime. See RFC 9111 §4.2.1.

Cache-Control: public, max-age=60, s-maxage=300
Age: 60

At Age 60, this is stale for a browser (60-second lifetime) and still fresh for a CDN (300 seconds). Age keeps climbing with transit and storage time. And no-cache means a cache has to revalidate before every reuse, regardless of the freshness lifetime.

Real-World Scenarios

Cloudflare reports its cache decision in CF-Cache-Status. Its cache-response documentation explains that an edge HIT can inherit Age from an upper tier. So a large Age doesn’t tell you how long the object has been sitting in that particular edge location.

For a public response in an existing Express app, set the freshness policy at the origin and let the caches work out Age:

app.get('/public-config', (req, res) => {
  res.set('Cache-Control', 'public, max-age=60, s-maxage=300');
  res.json({ theme: 'light' });
});

The 60 and 300 seconds are example values, not Express defaults. This route returns the same public data to everyone, which is what makes public safe. Put a user’s account details behind the same policy and shared caches will happily hand them to other users; no Age value fixes that. Express documents setting response fields in res.set().

Sometimes serving stale is the plan. With max-age=60, stale-while-revalidate=30, a cache that supports the extension can keep serving the response for another thirty seconds while it revalidates in the background. Age keeps reporting the real elapsed time during that window. It isn’t capped at sixty, and starting a background refresh doesn’t reset it. See RFC 5861 §3. Once the origin validation succeeds, the cache can update its stored metadata and age baseline.

Getting Age right

Leave Age to the caches. An application that sets Age: 0 to mean “uncached” is sending a misleading value, and a dashboard that counts every nonzero Age as a hit is miscounting. Read Age together with Date, Cache-Control, the validators, and the CDN’s cache status. A stale response might be served on purpose under a stale-serving policy, so Age alone doesn’t tell you something is broken.

Age isn’t a countdown, either. To estimate remaining freshness, subtract Age from the applicable lifetime, then subtract the time that has passed since you received the response.

Cache Age Analysis

Compare repeated requests for the same URL and the same representation. Differences in Vary inputs, which cache location you hit, evictions, and revalidation all change the result, so expect Age to jump around across unrelated requests rather than climb steadily.

This browser-console probe logs the relevant fields side by side instead of calling every nonzero Age a hit:

async function inspectAge(url) {
  const response = await fetch(url);
  const raw = response.headers.get('age');
  const age = raw !== null && /^\d+$/.test(raw) ? Number(raw) : null;
  console.table({
    status: response.status,
    ageSeconds: age,
    date: response.headers.get('date'),
    policy: response.headers.get('cache-control'),
    etag: response.headers.get('etag'),
    cache: response.headers.get('cf-cache-status') ??
      response.headers.get('x-cache'),
  });
  await response.arrayBuffer();
}
await inspectAge('/style.css');

A null field means the header was either absent or hidden by CORS. Keep it as null: converting it to zero turns “I couldn’t see this” into “this is brand new”. A plain fetch can also be served from the browser cache, so check the Network panel when you’re investigating the edge. The edge and the browser’s private cache may be applying different lifetimes to the same response.

Testing

Replace the URL with one of your cached assets. These commands send a GET and throw away the body:

curl -sS -D - -o /dev/null https://example.com/style.css
curl -sS -D - -o /dev/null https://example.com/style.css
curl -sS -D - -o /dev/null \
  -H 'Cache-Control: no-cache' https://example.com/style.css

The last request asks caches to revalidate before reusing their copy. That doesn’t force a fresh body download from the origin; a 304 from the origin is enough. From cross-origin browser JavaScript, note that Age isn’t a CORS-safelisted response header. The server has to send Access-Control-Expose-Headers: Age before response.headers.get('Age') will return it. See the Fetch Standard.

To watch Age change across a few requests from one shell, keep the URL and request headers fixed:

for attempt in 1 2 3; do
  curl -sS -D - -o /dev/null https://example.com/style.css
  sleep 1
done

The sleep is just a sampling interval. If Age drops, save the full output. A lower Age right after revalidation is expected. A lower Age alongside a different serving-node identifier usually means the request took a different cache path. If the body changed but the validator didn’t, that’s a separate bug at the origin. And if the browser shows different content from curl, compare the request URL, cookies, Accept-Encoding, and other Vary inputs before blaming Age; the two clients may be getting different stored representations.

Frequently asked questions

What is the Age header?

Age estimates the number of seconds since the origin server generated or last validated the response, including cache residence and transit time.

How is Age calculated?

A cache combines the origin Date, any Age it received from upstream, the request/response delay, and how long it has held the response itself. That is why Age is usually larger than the time since the nearest cache stored the response.

What does Age: 0 mean?

Age: 0 means the response is close to brand new: it was just generated or validated. That could be a cache miss or a hit on a freshly stored copy, so check the cache status header to tell which. It also says nothing about whether the application data behind it is current.

How does Age relate to max-age?

A response is stale when its current age reaches its applicable freshness lifetime. Shared caches use s-maxage in preference to max-age when present.

Sources

  1. MDN Web Docs: Agedeveloper.mozilla.org
  2. RFC 9111: Agerfc-editor.org
  3. RFC 9111: Calculating Agerfc-editor.org
  4. Cloudflare: Cache responsesdevelopers.cloudflare.com
  5. Fetch Standard: CORS-safelisted response headersfetch.spec.whatwg.org

Keep going

Browse /search