# Age Header

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

Source: https://howhttpworks.com/headers/age
Last reviewed: 2026-10-05

> **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](https://www.rfc-editor.org/rfc/rfc9111#section-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.

```http
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](https://www.rfc-editor.org/rfc/rfc9111#section-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:

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

```text
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](https://www.rfc-editor.org/rfc/rfc9111#section-4.2.1).

```http
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](https://developers.cloudflare.com/cache/concepts/cache-responses/) 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:

```javascript
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()`](https://expressjs.com/en/5x/api/response/#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](https://www.rfc-editor.org/rfc/rfc5861#section-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:

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

```bash
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](https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name).

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

```bash
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.

## Related Headers

- [Cache-Control](https://howhttpworks.com/headers/cache-control)
- [Expires](https://howhttpworks.com/headers/expires)
- [ETag](https://howhttpworks.com/headers/etag)
- [Via](https://howhttpworks.com/headers/via)
