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:
- Cache Debugging and Observability — the broader diagnostic toolkit
Agebelongs to, alongsideX-CacheandVaryinspection. - How to calculate cache freshness lifetime —
Ageis only meaningful relative to thefreshness_lifetimethis guide computes. - Mastering
max-ageands-maxagedirectives — the two directivesAgeis most commonly compared against.
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.Age≈freshness_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 ifstale-while-revalidateorstale-if-errorexplicitly authorizes it, ormust-revalidateis absent and the cache is falling back to stale-on-error. Otherwise,Age > freshness_lifetimeon a plainHITis 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
Agevalue without incrementing it for its own residency time, violating RFC 9111 §5.2.3’s requirement that every cache in the chain incrementsAgefor 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:
Ageis a non-negative integer, always present on any response served from a shared cacheHIT, and absent (or0) on a genuine origin miss.Ageclimbs monotonically between requests spaced within the freshness window, at approximately the same rate as wall-clock time between requests.Ageresets to a low value only immediately following a confirmed revalidation (304) or an explicit purge — never spontaneously.Agenever exceeds the effectivefreshness_lifetimeunless astale-*directive explicitly authorizes the overage — verify this pairing wheneverAge > max-age/s-maxageis 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 negativeAgewhen the origin’s clock runs ahead of the cache’s clock. If you seeAge: 0on every request despite an apparently working cache, check whether the origin’sDateheader is drifting ahead of real time. - Missing
Dateheader at origin — withoutDate, caches cannot computeapparent_ageat all and must fall back to treating the response as generated at receipt time, which understates true age on the first hop. Always emitDatefrom the origin. - HTTP/1.1 vs HTTP/2 — no semantic difference —
Agecomputation 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
Ageverbatim without adding their own residency time, silently understating true age. This is the most common cause of anAgevalue 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.
Related
- How to calculate cache freshness lifetime — the RFC 9111 algorithm that determines the denominator
Agemust be compared against. - Implementing stale-while-revalidate on Cloudflare — configures the one common, conformant case where
Agelegitimately exceeds the freshness lifetime. - How to debug 304 Not Modified responses — the revalidation event that correctly resets
Ageto a low value.