An HTTP response arrives at your cache with `Date: 10:00:00`, `Cache-Control: max-age=300` and `Age: 240`. How long can your cache serve it, and how is a stored response's current age actually computed?
answer
- max-age counts from origin generation, not arrival
- Age accumulates hop by hop
- corrected_initial_age = max(apparent, Age + delay)
- current_age = initial + resident time
- max() so bad clocks expire early, never late
basics
~20 sOnly about 60 more seconds. The Age header says the response is already 240 seconds old, and max-age=300 counts from origin generation, not from arrival. Current age = the age it arrived with (from Age or Date) plus the time it has since sat in your cache.
solid answer
~60 s`max-age` is measured from when the origin generated the response, not from when you received it. `Age: 240` says an upstream cache already held it for 240 seconds, so of the 300-second lifetime only ~60 seconds remain. RFC 9111 computes it in two parts. **Corrected initial age** is what the response was worth on arrival, taken as the larger of two estimates: the *apparent age* (`response_time - Date`, floored at zero) and the *corrected age value* (`Age` header + the request/response delay you observed). Taking the maximum defends against a skewed origin clock and against an upstream cache under-reporting Age. **Resident time** is `now - response_time`. Current age is their sum, and the response is fresh while current age is below the freshness lifetime. Every cache on the path adds its own holding time to `Age`, so `Age` grows monotonically toward the origin's `max-age`. That is why a CDN response can arrive nearly expired, and why a browser sometimes revalidates far sooner than the `max-age` suggests — nothing is wrong; the clock started upstream.
code
http · 8 linesHTTP/1.1 200 OK
Date: Mon, 12 Aug 2026 10:00:00 GMT
Age: 240
Cache-Control: max-age=300
ETag: "v7"
# generated 10:00:00, already 240s old on arrival
# ~60s of freshness left in this cachego deeper
Know that max-age is counted from when the origin made the response and that Age tells you how much is already used up.
Reproduce the arithmetic and explain why a CDN-served response can arrive with most of its lifetime already spent.
Use Age as a diagnostic, explain the max() defence against clock skew and dishonest intermediaries, and reason about the s-maxage/max-age split.
Treat origin clock discipline and Age propagation as operational invariants of the caching tier, and specify per-tier lifetimes rather than a single global number.
## The rule that surprises people `Cache-Control: max-age=300` does not mean "each cache may hold this for 300 seconds". It means the response may be reused until it is 300 seconds old, counted from the moment the origin generated (or last validated) it. If lifetimes restarted at every hop, a chain of three caches would turn a five-minute lifetime into fifteen, and the origin would have no control over how stale content could get. The `Age` header is the mechanism that prevents this. ## The Age header `Age` is a response header carrying the cache's estimate, in seconds, of how long it has been since the response was generated or successfully validated at the origin. An origin serving a fresh response sends no `Age` (equivalently zero). Every cache that stores and reuses a response emits `Age` reflecting its own holding time plus whatever it received. So `Age` accumulates along the path and is the shared unit of account across independent caches. ## The computation, in the RFC's terms Define `request_time` as when your cache sent the request and `response_time` as when it received the response. - `apparent_age = max(0, response_time - Date)` — how old the response looks by comparing its origin timestamp with your clock. Floored at zero because clock skew can make it negative. - `response_delay = response_time - request_time` — how long the fetch took. - `corrected_age_value = Age_header + response_delay` — the reported age, adjusted for the fact that the response spent the transfer time in flight after that value was computed. - `corrected_initial_age = max(apparent_age, corrected_age_value)` — the age on arrival. - `resident_time = now - response_time` — how long you have held it. - **`current_age = corrected_initial_age + resident_time`** The response is fresh while `current_age < freshness_lifetime`. ## Why the maximum of two estimates Each estimate has a failure mode and the maximum is the conservative choice. `apparent_age` depends on the origin's `Date` header agreeing with your clock. A misconfigured origin whose clock runs slow makes responses look older than they are; one running fast makes them look newer, which is why the value is floored at zero. `corrected_age_value` depends on upstream caches computing and forwarding `Age` honestly. An HTTP/1.0 intermediary that never emits `Age`, or a buggy proxy, would under-report. Taking the larger means a broken clock or a silent intermediary produces *earlier* expiry — an unnecessary revalidation — rather than serving content past the lifetime the origin authorised. Erring toward extra round trips is the right direction; erring toward extra staleness is not. ## Working the example `Date: 10:00:00`, `Age: 240`, `max-age=300`, and suppose the response arrives at 10:04:01 after a 1-second fetch. - `apparent_age = 10:04:01 - 10:00:00 = 241` - `corrected_age_value = 240 + 1 = 241` - `corrected_initial_age = 241` - Freshness lifetime is 300, so it is fresh while `241 + resident_time < 300`, i.e. for about **59 more seconds**. Then it goes stale and the next request triggers a conditional check. ## What this explains in practice **"Our CDN caches for an hour but browsers refetch after five minutes."** The CDN serves a response with a large `Age`; the browser subtracts it and gets a short remaining lifetime. If you want browsers to hold things longer independently of edge age, that is what `max-age` plus `s-maxage` is for — `s-maxage` governs the shared cache, `max-age` the browser, and the browser still subtracts `Age`. **Debugging staleness.** `Age` is the first header to read. A large `Age` on a response that should be fresh points at an intermediary holding it; `Age: 0` on every response points at a cache that is not caching at all. **Clock skew matters.** Because `apparent_age` compares the origin's `Date` with local time, an origin with a wrong clock systematically shortens or distorts cache lifetimes across the whole internet. NTP on origin servers is a caching concern, not just a logging one. **Heuristic freshness uses the same arithmetic.** When there is no explicit lifetime, whatever the cache derives is still compared against this same `current_age`, so `Age` accounting applies identically.
- Why does RFC 9111 take the maximum of the apparent age and the Age-based estimate rather than trusting one?Each estimate can be wrong in a way that under-reports age: the apparent age depends on the origin's clock agreeing with yours, and the Age-based value depends on every upstream cache reporting honestly. Taking the larger means any such fault causes the response to expire earlier than necessary, costing a revalidation, rather than being served past the lifetime the origin allowed.
- A CDN is configured to cache an object for one hour, but browsers seem to refetch after a few minutes. What is happening?The browser receives the object with a large `Age` reflecting how long the edge has held it, and subtracts that from `max-age`, so the remaining browser-side lifetime is short. This is correct behaviour, not a bug. If you want a long edge lifetime and an independent browser lifetime, use `s-maxage` for the edge and a separate `max-age`, remembering the browser still subtracts Age.
- What does it mean if every response from your cache carries `Age: 0`?It means the cache is not reusing stored responses — every request is being answered by a fresh origin fetch. Common causes are a `Vary` header or cache key that never matches, `no-store` or `private` on the responses, cookies on requests defeating the edge's caching rules, or a lifetime of zero. It is the standard first signal that a cache is effectively disabled.
saying these in an interview costs you the question
- Thinking each cache in the chain gets a full max-age window of its own
- Treating age as time since the local cache stored the response
- Ignoring the Age header when debugging premature expiry
- Assuming an origin's clock skew has no effect on caching
- Believing a 304 leaves the age unchanged — successful validation resets it