Age Header
The Age response header is a cache's estimate, in seconds, of how long ago the origin generated or last validated the response being served. Caches generate it, not the origin, and the value accumulates across every cache in the path. A response is stale once Age reaches its freshness lifetime.
Full Explanation
The Age response header is a cache's report of how old the response it just handed you is. RFC 9111 section 5.1 defines the field value as "the sender's estimate of the time since the response was generated or successfully validated at the origin server". The value is counted in whole seconds. A cache is required to generate the header when it answers a request from its store without validating first. It must replace any value already present. So the number always comes from a cache in the path: a CDN edge server, a reverse proxy, or a browser cache, not the origin. A conformant origin sends Date, not Age. The presence of Age "implies that the response was not generated or validated by the origin server for this request".
It is telemetry, not an instruction. Cache-Control is the header that tells caches what to do. RFC 9111 section 5.2 describes it as a list of "directives for caches along the request/response chain". Age only reports what a cache already did. It is the one standard header that does. Compare it against the freshness lifetime the response was given. That lifetime is normally max-age, or s-maxage at a shared cache such as a CDN. Age then tells you how much of that lifetime is left.
Two things Age is not. First, it is not a per-cache counter. In the specification, the value accumulates over every cache the response has passed through, plus time in transit. Second, it is not a hit-or-miss flag. A missing Age does not prove the origin was contacted. Cloudflare, for one, omits the header entirely on a miss rather than sending Age: 0.
How it works
Age is calculated per response served, not stored as a fixed number. RFC 9111 section 4.2.3 gives the arithmetic.
- The cache measures how old the response is on arrival. apparent_age = max(0, response_time - date_value). This shows how old the response looks by the clock. corrected_age_value = age_value + response_delay. This adds any Age already on the response to the time the fetch took. The conservative starting point is the larger of the two: corrected_initial_age = max(apparent_age, corrected_age_value).
- On every hit, the cache adds the time since it received the copy. resident_time = now - response_time. current_age = corrected_initial_age + resident_time. That current_age is the value sent in the Age header.
- The result is a chain total. In the RFC's words, Age is "the sum of the time that the response has been resident in each of the caches along the path from the origin server, plus the time it has been in transit along network paths".
- Successful validation restarts the count. Age runs from generation or from the last successful validation. So a 304 Not Modified answer to a conditional request built on an ETag resets it.
Freshness is the comparison between that age and the lifetime. RFC 9111 section 4.2 puts it as response_is_fresh = (freshness_lifetime > current_age). So equality already counts as stale. The lifetime comes from the first rule that matches, per section 4.2.1. The order is: s-maxage if the cache is shared, otherwise max-age, otherwise Expires minus Date, otherwise a heuristic the cache chooses. So max-age minus Age gives the remaining freshness only when max-age is the directive in force. With max-age=3600 and Age: 3500 there are 100 seconds of TTL left. At Age: 3600 the copy is stale.
You can see this on the wire with curl.
# Check Age header with curl
$ curl -sI https://cdn.example.com/style.css | grep -i 'age\|cache-control'
Cache-Control: max-age=86400
Age: 7243
# This copy is 7243 seconds old, about 2 hours, summed over every cache in the path
# Remaining freshness: 86400 - 7243 = 79157 seconds (~22 hours)
# Age: 0 means a cache stored or revalidated this copy moments ago.
# It is not proof of a miss: an Age header means the origin did not answer this
# request (RFC 9111 section 5.1), and Cloudflare sends no Age at all on a MISS.
$ curl -sI https://cdn.example.com/new-page.html | grep -i '^age:'
Age: 0
The syntax is strict, and the edge cases are specified. The value is a non-negative integer of seconds. RFC 9111 section 1.2.2 covers overflow. If a cache receives a value larger than it can represent, or its own age arithmetic overflows, it must use 2147483648 (2 to the 31st, "over 68 years") or the largest positive integer it can conveniently represent. What matters is "that an overflow be detected and not treated as a negative value in later calculations". A cache that receives a list of values should use the first and discard the rest. It should ignore the field altogether if the value is not a non-negative integer.
Stale does not mean the client was refused. Under stale-while-revalidate, RFC 5861 section 3 lets a cache serve the response after it becomes stale, up to the number of seconds given. The cache should attempt to revalidate it "while still serving stale responses (i.e., without blocking)". The same section notes that "stale" implies the response "will have a non-zero Age header". So stale content arrives with an Age past the lifetime. RFC 5861 pairs that with a warning header. RFC 9111 section 5.5 has since obsoleted Warning, noting that what it carried "can be gleaned from examining other header fields, such as Age". That leaves Age as the only in-protocol staleness signal left on the wire.
Why it matters for a CDN
- It is the only standard, cross-vendor evidence that a cache answered. Hit and miss headers, such as cf-cache-status and X-Cache, are vendor conventions, not HTTP. Age is defined by the specification. It means the same thing through any chain.
- Downstream caches subtract it from your TTL. Fastly documents exactly this. A backend response carrying Cache-Control: max-age=30 with Age: 10 produces an object TTL of 20 seconds. A request 8 seconds later hits with an age of 18 and a TTL of 12. An inflated Age from anything in front of a cache shortens how long that cache keeps the object.
- It exposes the tiers behind the edge. The specified value is cumulative. So an object fetched through an origin shield or a tiered parent can reach the edge already aged. The client sees that total rather than the edge's own residency.
- Rising Age across repeat requests is the confirmation that caching engaged. Cloudflare's own procedure checks that a response reaches cache. The procedure is to request the URL twice from the same client and look for HIT plus "an Age header that increases on subsequent requests". That is the practical test behind any cache hit ratio investigation. It is also the fastest way to catch a cache key that varies per request.
What CDNs do
Vendors implement Age to their own cache model. The differences change how you read the number. Behaviour from each vendor's current documentation:
- Cloudflare reports residency in its own cache rather than a chain total. Age "specifies the time in seconds that an asset has been in Cloudflare's cache", and "this value resets if the asset is revalidated, purged, or evicted and then re-cached". It is "only present for responses served from the cache". It is absent on a MISS, on dynamic traffic, on a response a Worker generated without going to cache, and on "the first request that populates the lower tier HIT from tiered cache".
- Fastly treats an inbound Age as a deduction from TTL, as above. It stores that value with the object. After a 304 revalidation, it resets Age "to the value included in the revalidation response, or zero if there is no Age header on the new response". When the TTL is set by an Expires header rather than max-age, Fastly initialises the object's Age to zero regardless of what the backend sent. While stale content is served, Age may exceed the object's TTL.
Watch out for
- No Age does not mean no cache. RFC 9111 section 5.1 is explicit that "lack of an Age header field does not imply the origin was contacted". Cloudflare's tiered cache fill is a concrete case: the lower tier serves the response with no Age at all.
Age: 0is not a miss diagnosis. A just-stored or just-revalidated copy is legitimately zero. A vendor that omits the header on a miss never sends zero in the first place. Classify with the vendor's status header instead. Cloudflare's guide to a URL that is served from origin every time works from cf-cache-status: DYNAMIC for not eligible, BYPASS for an uncacheable origin response, repeated MISS for key variance or eviction. Use Age only to see whether a stored copy is ageing.- Vendor Age is not always the RFC chain total. Cloudflare's value is residency in one cache, and it resets on revalidation. Do not add Age across a mixed-vendor chain, or read it as total time since the origin generated the object. Check each vendor's definition first.
- It is an estimate, and it depends on clocks. The apparent_age branch is usable only "if the implementation's clock is reasonably well synchronized to the origin server's clock" (RFC 9111 section 4.2.3). A wrong Date at the origin corrupts every age calculation downstream of it.
- max-age minus Age is the wrong sum at a CDN when s-maxage is present. For a shared cache, s-maxage "overrides the maximum age specified by either the max-age directive or the Expires header field" (RFC 9111 section 5.2.2.10). So subtract the directive that actually governs the tier in front of you. Under heuristic freshness there is no header to subtract from at all.
- Age tells you time, never which tier answered. A large value could be the edge, a shield, a second CDN, or a corporate proxy. Only a per-hop status header attributes it.
- An origin that sets Age itself damages downstream freshness. The first cache to serve the response from store overwrites the value. Until then, every cache treats it as real age and shortens the object's life by that amount.
Best practice
- Never set Age at the origin. Set the lifetime instead: s-maxage for shared caches, max-age for browsers. Let each cache report age for itself.
- Send an accurate Date header. Keep origin and cache clocks synchronised. Every age calculation in the chain derives from Date.
- Log Age together with the vendor's cache status header. Neither is sufficient alone. The status names the decision. Age shows how long the stored copy has been alive.
- Test caching by requesting the same URL twice from the same location. Confirm that Age increases. A second response with no Age, or with Age back at zero, means the copy was not reused.
- Track Age against the lifetime you set. For a long-lived object whose Age never climbs near its lifetime, suspect eviction or cache-key variance rather than a tuning problem. Cloudflare notes that low-traffic assets can be evicted before the next request arrives. It points at Tiered Cache or Cache Reserve for long-tail content.
- Expect Age to restart after a purge or a revalidation. Treat that reset as normal rather than as a caching failure.
Examples
Use the Age header with other cache headers when you debug a CDN.
# Full cache debugging request
$ curl -sI https://cdn.example.com/api/products | grep -iE 'age|cache|x-cache|cf-cache'
Cache-Control: public, max-age=300
Age: 45
X-Cache: HIT
# Content was cached 45 seconds ago, still fresh for 255 seconds
# X-Cache: HIT confirms it was served from cache
Frequently Asked Questions
The Age response header is a cache's estimate, in seconds, of how long ago the origin generated or last validated the response being served. Caches generate it, not the origin, and the value accumulates across every cache in the path. A response is stale once Age reaches its freshness lifetime.
Use the Age header with other cache headers when you debug a CDN.
# Full cache debugging request
$ curl -sI https://cdn.example.com/api/products | grep -iE 'age|cache|x-cache|cf-cache'
Cache-Control: public, max-age=300
Age: 45
X-Cache: HIT
# Content was cached 45 seconds ago, still fresh for 255 seconds
# X-Cache: HIT confirms it was served from cache
Related CDN concepts include:
- max-age — max-age is a Cache-Control response directive giving the seconds a stored response may be reused …