Freshness and caching
Cached or live? The response fields that say where a page came from
Read Age, Cache-Status and CDN cache headers to tell whether a fetched page came from a cache, estimate how old it is and log the right fields with it.
By Curtis Mccann, Live Web Data team · Published
This article was drafted with AI assistance from the sources listed below and edited and published by the Live Web Data team. Disclosure: the Live Web Data API, which this article mentions, is this site's own product.
- Your fetch time: UTC, taken by your own code when the response arrives
- Cache layer of your fetch tool: Read live, or served from the tool's own store
- Age: Seconds since the origin generated or validated the response, when present
- Cache-Status or CDN cache header: Hit or forward, the reason, and the remaining ttl
- Estimated copy age: The conservative choice: the larger of the Date-based and the Age-based value
Your fetch tool says it read the page live, and your log shows a fetch time from a minute ago. The model is told the page is current. But the page can still be 10 minutes old, or several hours, because a cache in front of the site answered the request. Before you trust a page as current, check the fields that say where the copy came from.
Two caches, two questions
There are two places a copy can come from before it reaches your prompt.
The first is your fetch tool. Some tools and services keep their own store and return a stored copy when it is young enough. If yours does, its response should say so per URL: a source or cache-state field and the time the copy was read. If it says nothing, ask, because a fetch time without a source field cannot tell you which of the two you got.
The second sits in front of the site: a CDN or reverse proxy. Even a fetch that goes all the way out to the network can be answered by that cache, without the origin server being asked. Your tool's fetch time is then the time the CDN answered, not the time the page was generated. Only the HTTP response headers show this, so they are worth keeping when your tool exposes them.
Age: seconds since the origin answered
RFC 9111 defines Age as the sender's estimate of the time since the response was generated or validated at the origin server. The value is a non-negative integer in seconds, so Age: 600 is 10 minutes. Its presence means the origin did not generate or validate this response for your request. Its absence proves nothing: the RFC states that a missing Age does not mean the origin was contacted.
RFC 9111 also shows how a cache estimates the age of a copy. It uses corrected_age_value by default, and gives taking the larger of these two values as the more conservative calculation for when very old caches may be on the path:
apparent_age: the time the response arrived minus itsDateheader, never below 0.corrected_age_value: theAgevalue plus the response delay (arrival time minus request time).
A worked example. Your code sends the request at 09:00:01 UTC and the response arrives at 09:00:02. It carries Date: Fri, 13 Nov 2026 08:50:02 GMT and Age: 600. The apparent age is 600 seconds. The corrected age value is 600 + 1 = 601 seconds. The copy was about 601 seconds old when it arrived, roughly 10 minutes, even though your fetch time reads 09:00:02. The Date part depends on clocks being in sync; the RFC uses it only when the cache's clock is reasonably well synchronized with the origin's.
Cache-Status: one entry per cache
RFC 9211, published in June 2022, defines Cache-Status, a header with standardized syntax and semantics for what caches used to report in their own ad hoc headers. The value is a list with one member per cache that handled the request. The first member is the cache closest to the origin server and the last is the one closest to you. Every parameter is optional, so expect partial answers:
| Parameter | Meaning |
|---|---|
hit | The cache answered without forwarding the request |
fwd | The request went on to the next hop, with a reason |
fwd-status | The status code the next hop returned |
ttl | Remaining freshness in seconds; negative means stale |
stored | Whether the cache stored the forwarded response |
Reasons for fwd include uri-miss (nothing stored for the URL), vary-miss (a stored response matched the URL but not the request headers), miss (when the two cannot be told apart), stale (the stored response was stale) and request (a fresh copy existed but the request directives did not allow it).
Three readings to know:
ExampleCache; hit; ttl=376: served from the cache, with 376 seconds of freshness left.ExampleCache; hit; ttl=-412: served from the cache, 412 seconds past its freshness lifetime.ExampleCache; fwd=stale; fwd-status=304: the copy was stale, the cache asked the origin, and the origin said it had not changed.
RFC 9211 also notes that a cache may omit the header, or send it only to authorized clients.
CDN cache headers
Before Cache-Status, CDNs added their own headers. Cloudflare's CF-Cache-Status is one such header. Its documented values map onto the same questions:
HIT: found in Cloudflare's cache.MISS: cacheable, but not in the cache at the time of the request.EXPIRED: found in the cache but expired, so served from the origin.STALE: served from the cache although it had expired.UPDATING: expired, served from the cache while the origin is asked for a new copy in the background.REVALIDATED: the origin confirmed with a conditional request that the cached copy was unchanged.DYNAMIC: not eligible for the cache.
STALE and UPDATING are the two to watch: the content you got is older than the cache's own freshness limit.
What to log next to the content
Keep one record per fetched page:
{
"url": "https://example.com/status",
"fetched_at": "2026-11-13T09:00:02Z",
"tool_source": "live",
"age_header": 600,
"cache_status": "ExampleCache; hit; ttl=376",
"estimated_copy_age_seconds": 601
}
Pass the estimated copy age to the model with the content, not only your fetch time. For a status page, a copy that is 601 seconds old may already be wrong. For a docs page, it is fine. How old is acceptable per question is the decision in the live vs cached web data guide, and the Last-Modified and ETag post covers the other two headers people read as freshness signals.
On Live Web Data API
On our own API, the first of the two caches does not exist: Every request fetches the page live; there is no cache. Each URL gets a statuses[].fetchedAt with the time the page was read, which fills the fetched_at field above. The API reference lists every response field.
Frequently asked questions
Does a missing Age header mean the page was read from the origin server?
No. RFC 9111 says an Age header means the origin did not generate or validate the response for this request, but its absence does not prove the origin was contacted. Look for a Cache-Status or CDN cache header as well.
What does a negative ttl in Cache-Status mean?
The ttl parameter is the remaining freshness in seconds as the cache calculates it. A negative value means the cache served a response that was already stale, for example ttl=-412 is 412 seconds past its freshness lifetime.
In a Cache-Status header with several entries, which cache is which?
Each list member is one cache that handled the request. The first member is the cache closest to the origin server and the last is the cache closest to the client.
If my fetch tool reads the page live, is the content always seconds old?
Not always. A live read from your tool can still be answered by a CDN in front of the site. An Age header or a hit in Cache-Status shows that the copy is older than your fetch time.
Sources
- RFC 9211: The Cache-Status HTTP Response Header Field (Documentation)
- RFC 9111: HTTP Caching (Documentation)
- Cloudflare cache responses (CF-Cache-Status) (Documentation)