How to Read and Interpret the Age Header

A response arrives with Age: 41623 and the natural question is whether that number is fine, alarming, or meaningless without more context. Age is the one caching diagnostic header RFC 9111 actually standardizes, and it is frequently misread as “seconds since my request” rather than what it actually is: cumulative residency time across every cache between the origin and you. Misreading it leads to false alarms over healthy long-lived caches and, worse, missed detection of genuinely stale-serving bugs.

Prerequisite Concepts

Before working through the computation, make sure you’re comfortable with:

Step-by-Step Resolution

Step 1 — Capture Age alongside Date and Cache-Control

Age on its own tells you almost nothing. Always capture it together with Date and Cache-Control:

curl -sI https://example.com/api/products/9472 \
  | grep -iE '(^age|^date|cache-control|cf-cache-status|x-cache)'

A representative response:

HTTP/2 200
Date: Sun, 06 Jul 2026 14:32:10 GMT
Cache-Control: public, s-maxage=3600
Age: 812
CF-Cache-Status: HIT

Step 2 — Understand what Age actually represents (RFC 9111 §5.1)

Per RFC 9111 §5.1, Age is defined as the sender’s estimate of the number of seconds since the response was generated or successfully validated at the origin. It is generated fresh by every cache that stores the response — the header value transmitted is not a static timestamp but a live estimate recalculated on each forward.

The normative computation, per §5.2.3, works in two parts. First, each cache computes an apparent_age:

apparent_age = max(0, response_time - date_value)

Where response_time is the local clock time the response was received, and date_value is the parsed Date header from the origin. This guards against clock skew producing a negative value.

Next, the cache derives corrected_age_value by adding any age_value already present in the incoming response (i.e., Age set by an upstream cache) plus the estimated network delay:

corrected_age_value = age_value + response_delay
corrected_age = max(apparent_age, corrected_age_value)

The cache then stores corrected_age as its baseline, and increments it by one for every second the response sits in storage before being served again. The Age header ultimately sent to a client is:

resident_time = now - time_response_was_stored
current_age = corrected_age_initial + resident_time

Step 3 — Compare Age against the freshness lifetime

Once you have Age, compare it against the effective freshness_lifetime (from s-maxage for shared caches, max-age otherwise, per the freshness calculation guide):

remaining_freshness = freshness_lifetime - Age

For s-maxage=3600 and Age: 812: remaining_freshness = 3600 - 812 = 2788 seconds — comfortably fresh.

Three states matter:

  • Age < freshness_lifetime — fresh, normal. No action needed.
  • Agefreshness_lifetime (within a few seconds) — the entry is at the freshness boundary; expect the next request to trigger revalidation or a full refetch.
  • Age > freshness_lifetime — stale being served. This is only conformant behavior if stale-while-revalidate or stale-if-error explicitly authorizes it, or must-revalidate is absent and the cache is falling back to stale-on-error. Otherwise, Age > freshness_lifetime on a plain HIT is a cache bug — the cache failed to revalidate or evict an expired entry.

Step 4 — Confirm resetting vs. climbing behavior

Age should climb steadily between revalidations, not jump erratically or freeze. Verify with two spaced requests:

curl -sI https://example.com/api/products/9472 | grep -i '^age'
sleep 15
curl -sI https://example.com/api/products/9472 | grep -i '^age'

Expected: the second Age value is roughly 15 seconds higher than the first. A few possible abnormal outcomes and what each means:

  • Age climbs by exactly the sleep interval — healthy: the cache is holding one entry, and no revalidation occurred in between.
  • Age resets to a low or zero value — either a 304 revalidation refreshed the entry (expected, and healthy if it happens near the freshness boundary), or the entry was evicted and refetched entirely (expected after a purge, unexpected otherwise).
  • Age stays frozen at the same value across requests spaced further apart than the freshness lifetime — a broken intermediary is very likely passing through an upstream Age value without incrementing it for its own residency time, violating RFC 9111 §5.2.3’s requirement that every cache in the chain increments Age for its own storage duration.

Step 5 — Check for multi-tier accumulation

When a shield tier sits between the edge and the origin, each tier adds its own residency time on top of what it received. A request that shows Age: 3400 at the edge, where the shield alone has held the object for 3000 seconds and the edge PoP for 400 more, is behaving correctly — the two residencies sum. Confirm this by comparing Age at each tier if your CDN exposes per-hop values (some chain X-Cache tokens alongside a single summed Age; see interpreting X-Cache and CF-Cache-Status for how vendors expose per-tier detail).

# Compare Age at edge vs after forcing a shield-only path, if your CDN supports a debug header
curl -sI -H "Fastly-Debug: 1" https://example.com/api/products/9472 \
  | grep -iE '^age|fastly-debug'

Expected Output / Verification

A correctly functioning Age header displays these characteristics:

  • Age is a non-negative integer, always present on any response served from a shared cache HIT, and absent (or 0) on a genuine origin miss.
  • Age climbs monotonically between requests spaced within the freshness window, at approximately the same rate as wall-clock time between requests.
  • Age resets to a low value only immediately following a confirmed revalidation (304) or an explicit purge — never spontaneously.
  • Age never exceeds the effective freshness_lifetime unless a stale-* directive explicitly authorizes the overage — verify this pairing whenever Age > max-age/s-maxage is observed.
HTTP/2 200
Date: Sun, 06 Jul 2026 14:47:22 GMT
Cache-Control: public, s-maxage=3600
Age: 1727
CF-Cache-Status: HIT

1727 < 3600 confirms the entry is fresh with 1873 seconds remaining — a healthy state requiring no action.

Edge Cases

  • Clock skew between origin and cache — RFC 9111’s apparent_age = max(0, response_time - date_value) formula exists specifically to prevent a negative Age when the origin’s clock runs ahead of the cache’s clock. If you see Age: 0 on every request despite an apparently working cache, check whether the origin’s Date header is drifting ahead of real time.
  • Missing Date header at origin — without Date, caches cannot compute apparent_age at all and must fall back to treating the response as generated at receipt time, which understates true age on the first hop. Always emit Date from the origin.
  • HTTP/1.1 vs HTTP/2 — no semantic differenceAge computation is identical regardless of transport version; HTTP/2’s multiplexing does not change cache residency semantics.
  • Reverse proxies that don’t implement §5.2.3 correctly — some lightweight proxies pass through an upstream Age verbatim without adding their own residency time, silently understating true age. This is the most common cause of an Age value that looks “too low” for how long content has clearly been unchanged.

Frequently Asked Questions

What exactly does the Age header measure?

It measures the number of seconds since the response was generated or last validated at the origin, as estimated cumulatively by every cache between the origin and the client — not the time since the current client’s request was made.

Can Age be larger than max-age?

Yes, but only conformantly when stale-while-revalidate, stale-if-error, or an equivalent stale-serving grace period explicitly authorizes it. Outside that, Age exceeding the effective max-age or s-maxage on a HIT indicates the cache failed to revalidate or evict an expired entry — a bug worth escalating.

Why does Age reset to 0 sometimes and not others?

A reset following a 304 revalidation or a deliberate purge is expected and healthy. A reset with no corresponding event in your logs suggests the cache isn’t actually persisting the entry between requests — check for Vary fragmentation or an unintended no-store.

How does Age behave across multiple cache tiers?

Each tier in the chain must add its own storage duration to the age_value it received from the tier above it, per RFC 9111 §5.2.3. The final Age a client sees is the sum of residency time across every tier the response passed through — a shield holding an object for an hour before the edge holds it for another minute correctly reports Age near 3660 seconds, not 60.


Back to Cache Debugging and Observability