Skip to main content
sitewarm

How to read Age, Cache-Control and X-Cache

· sitewarm

A response that comes through a CDN carries headers that say what the cache did with it. Three are worth knowing: Cache-Control, Age, and a status header such as X-Cache. This article explains what each one says according to the standards and the CDN vendors' own documentation, how to read them together, and what none of them tells you.

Everything here comes from RFC 9111, RFC 9211, MDN, and the Fastly and Cloudflare documentation, read on 2026-10-24. The example responses are illustrative and were not captured from a real site.

What each header is for

HeaderWho sets itWhat it saysWhere it is defined
Cache-ControlThe origin, and sometimes the CDNHow caches may store and reuse the responseRFC 9111
AgeA cache that serves a stored responseThe sender's estimate of the seconds since the response was generated or validated at the originRFC 9111, section 5.1
X-Cache and similarThe CDNThe CDN's own report of hit or miss, in its own wordsThe CDN's documentation (not a standard)
Cache-StatusEach cache that handled the requestA standard, structured report from each cacheRFC 9211

The first one is an instruction. The other three are reports. Reading a response well means keeping those two apart: Cache-Control says what a cache is allowed to do, and the reports say what one cache did.

Cache-Control: what the cache may do

RFC 9111 defines the directives a response can carry. These are the ones that matter when you read a CDN response:

  • `max-age`: the response is considered stale after its age is greater than the given number of seconds.
  • `s-maxage`: applies to shared caches such as CDNs, and overrides max-age there. A page can ask browsers and the CDN for different lifetimes.
  • `no-cache`: the response may be stored, but it must not be used to satisfy another request without being forwarded for validation first.
  • `no-store`: a cache must not store any part of the request or the response.
  • `private`: a shared cache must not store the response, because it is meant for a single user. RFC 9111 notes that this controls where a response can be stored, and cannot ensure the privacy of its content.

A response with no-store or private is not meant to be a cache hit at a CDN. A response with no-cache can be stored, but every use needs a check with the origin. When a page you expected to be cached never is, Cache-Control is the first header to read.

Age: how old the stored copy is

RFC 9111 says that when a cache uses a stored response without validating it, the cache must generate an Age header. The value is a non-negative integer, in seconds, and it conveys the sender's estimate of the time since the response was generated or successfully validated at the origin server. MDN describes it as the time in seconds that an object was in a proxy cache.

Age is what lets you compare a stored copy with its lifetime. RFC 9111 defines a response as fresh while its age has not exceeded its freshness lifetime. With Cache-Control: s-maxage=300 and Age: 42, the copy is fresh, and by this measure it has about 258 seconds left. That arithmetic holds when s-maxage is the freshness lifetime that applies. Other headers and CDN settings can change the lifetime.

When Age is zero or missing

MDN says the value is usually close to zero, and that 0 means the object was probably fetched from the origin server. Cloudflare documents a stricter rule for its own network: it sets Age on HIT, STALE and UPDATING responses, and does not set it on MISS. An absent Age on a first request is normal. It is a reason to request the page a second time, not a sign of a fault.

When Age resets

Cloudflare says its Age counts the seconds an asset has been in its cache since it was admitted or last revalidated, and that it resets on revalidation against the origin, on purge, or on eviction. So an Age that grows between two requests suggests the same stored copy, and a sudden small value suggests the copy was refreshed or replaced. This is Cloudflare's documented behavior. Other CDNs document their own.

Cloudflare also notes that with Tiered Cache, a local HIT can carry an Age inherited from an upper tier. The number is the age of the copy, not the time it spent in the cache that answered you.

X-Cache and other status headers

No RFC defines X-Cache. Fastly calls it a non-standard header that it appends to all responses by default. Each CDN picks its own header name and its own values, so the same word can mean slightly different things on different networks.

Fastly: X-Cache

Fastly's X-Cache is a simplified form of its internal state. It reads HIT when the state is HIT or starts with HIT-, and MISS otherwise. Requests that end in a pass show as MISS. Content generated at the edge, and hits that came from serving stale or from background revalidation, show as HIT. The header may contain reports from multiple servers when shielding is enabled. Outside of restarts, Fastly says any value other than MISS means the request was served from cache and did not reach the origin.

Cloudflare: cf-cache-status

Cloudflare uses cf-cache-status, with more values than hit and miss:

  • HIT: the resource was found in Cloudflare's cache.
  • MISS: it was not in the cache at request time, so it was served from the origin.
  • EXPIRED: it was in the cache but expired, so it was served from the origin.
  • STALE: it was served from the cache while expired, because Cloudflare could not contact the origin.
  • REVALIDATED: the origin confirmed in a conditional request that the cached copy was unchanged.
  • UPDATING: expired content was served from the cache while the origin updates it in the background.
  • BYPASS: the resource was eligible for caching at request time, but the origin response was not cacheable.
  • DYNAMIC: Cloudflare determined at request time that the asset is not eligible for cache, so no cache lookup happened.

A DYNAMIC page is not a cold cache. It is a page that the CDN is not going to cache under its current configuration, and requesting it again will not change that.

Cache-Status: the standard version

RFC 9211 defines Cache-Status to indicate how caches have handled a response and its request. Its value is a list, with one member for each cache that handled the request, and each member names its cache. Parameters report whether the request was satisfied by the cache (hit), why it was forwarded (fwd, with values such as uri-miss, vary-miss, stale and bypass), the response's remaining freshness lifetime (ttl), and whether the response was stored (stored). Whether a response carries this header depends on the CDN. Check its documentation.

Reading the three together

These two responses are illustrative. They are written to show the reading, not captured from a real site. The first request fills the cache; the second is served from it.

# First request
HTTP/2 200
cache-control: public, s-maxage=300
cf-cache-status: MISS

# Second request, a moment later
HTTP/2 200
cache-control: public, s-maxage=300
cf-cache-status: HIT
age: 42

In the first response, the status is MISS and there is no Age: the origin served this request, and the CDN stored the response because Cache-Control allowed it. In the second, the status is HIT and Age is 42: the CDN served a stored copy that is 42 seconds old, inside its 300-second lifetime. If the second request had also been a MISS, the next header to read would be Cache-Control.

What you seeWhat it usually means
Status HIT and an Age valueA stored copy answered. Age is its age, and s-maxage or max-age is its lifetime.
Status MISS, no AgeThe origin answered. The CDN may have stored the response for the next request.
Cache-Control: no-store or privateA shared cache is not meant to store the response, so expect no HIT.
cf-cache-status: DYNAMIC (Cloudflare)Not eligible for cache under the current configuration. No lookup happened.
cf-cache-status: BYPASS (Cloudflare)Eligible at request time, but the origin response was not cacheable.
Age jumps back to a small numberOn Cloudflare, a revalidation, purge or eviction. Check your own CDN's documentation.

What these headers do not tell you

  • They describe one request. A HIT now does not mean the next request is a HIT. The stored copy can expire, be purged or be evicted.
  • They describe the cache that answered. Cloudflare divides its data centers into lower tiers and upper tiers: when a lower tier misses, it asks an upper tier, and only the upper tier contacts the origin. Fastly says X-Cache can hold reports from several servers when shielding is on. Which cache answers can depend on where the request enters the CDN.
  • Words differ between CDNs. X-Cache: HIT on Fastly includes hits from serving stale. A HIT from a CDN that reports stale separately may mean fresh only. Read the vendor's definition before you compare.
  • Headers do not say why a page was not cached. They say that it was not. For the reason, read Cache-Control, the cookies and Vary on the response, and the CDN's cache rules.

A practical habit follows from this. Request the page twice, read all three headers on both responses, and note where the request came from. One request and one location show one cache, and that is all they show.

Where sitewarm fits

sitewarm requests pages from your sitemap, up to the scan's URL limit, and reports the cache state the CDN returns for each one, with the vantage point it was measured from.

Frequently asked questions

What does the Age header mean?

Age conveys the sender's estimate of the time, in seconds, since the response was generated or successfully validated at the origin server. A cache that serves a stored response without validating it must send it. See Age.

What does X-Cache: HIT mean?

X-Cache is not a standard header, so the meaning is the CDN's own. Fastly documents that HIT means the state was a hit, including hits from serving stale or background revalidation, and that outside of restarts any value other than MISS means the request was served from cache and did not reach the origin.

Why is there no Age header on a cache miss?

A miss is answered by the origin, so there is no stored copy to age. MDN says a value of 0 means the object was probably fetched from the origin server, and Cloudflare documents that it does not set Age on MISS.

Is there a standard header for cache status?

Yes. RFC 9211 defines Cache-Status, a list with one member for each cache that handled the request. CDNs that do not send it use their own header, such as X-Cache or cf-cache-status. See Cache-Status.

References

  1. RFC 9111: HTTP Cachinghttps://www.rfc-editor.org/rfc/rfc9111
  2. RFC 9211: The Cache-Status HTTP Response Header Fieldhttps://www.rfc-editor.org/rfc/rfc9211
  3. MDN: Agehttps://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Age
  4. MDN: Cache-Controlhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control
  5. Fastly: X-Cachehttps://www.fastly.com/documentation/reference/http/http-headers/X-Cache/
  6. Cloudflare: Cloudflare cache responseshttps://developers.cloudflare.com/cache/concepts/cache-responses/
  7. Cloudflare: Tiered Cachehttps://developers.cloudflare.com/cache/how-to/tiered-cache/

See the cache state your CDN returns for each page

sitewarm requests pages from your sitemap, up to the scan's URL limit, after a deploy and reports the cache state the CDN returns for each one, with the vantage point it was measured from.

Start warming