no-cache vs max-age=0: What’s the Difference?
Two responses — one sent with Cache-Control: no-cache and the other with Cache-Control: max-age=0 — often produce identical behavior in a browser’s network panel, which leads engineers to treat them as interchangeable. They are not defined the same way in RFC 9111, and the gap between their definitions surfaces exactly when a cache cannot reach the origin to revalidate.
Prerequisite Concepts
Before comparing the two directives, make sure you’re comfortable with:
- no-cache vs no-store: When to Use Each — the broader distinction between “store but always revalidate” and “never store.”
- Mastering
max-ageands-maxagedirectives — how the primary freshness directive is calculated and applied. - Header stacking and directive precedence — how multiple directives on the same response interact.
Step-by-Step Resolution
Step 1 — Read what each directive actually mandates
no-cache (RFC 9111 §5.2.2.4) says a cache must not use a stored response to satisfy a request without successful revalidation with the origin. This is an unconditional instruction: it applies every time, regardless of whether the cache can currently reach the origin.
max-age=0 (RFC 9111 §5.2.2.1) is a freshness value, not a revalidation mandate. It tells the cache that the freshness_lifetime is zero seconds, meaning the response is stale the instant it is stored. RFC 9111 §4.2.4 then says: once stale, a cache should revalidate before use — but it also permits a cache to serve a stale response anyway when it cannot reach the origin (for example, the network is down), unless must-revalidate or proxy-revalidate is present.
That “should, unless disconnected” language is the entire difference. no-cache closes that escape hatch by definition; bare max-age=0 does not.
# Definition A — unconditional revalidation, no disconnected-serving exception
Cache-Control: no-cache
# Definition B — immediately stale, but may be served stale if origin is unreachable
Cache-Control: max-age=0
Step 2 — Add must-revalidate to close the gap
To make max-age=0 behave identically to no-cache, pair it with must-revalidate:
Cache-Control: max-age=0, must-revalidate
RFC 9111 §5.2.2.2 defines must-revalidate as removing the disconnected-serving allowance entirely: once the response is stale (which it is immediately, since max-age=0), the cache must not use it without successful revalidation, and must return a 504 Gateway Timeout (or similar error) rather than serve stale content if it cannot reach the origin. This is functionally identical to what no-cache mandates on its own. In practice, max-age=0, must-revalidate is the classic pre-RFC-9111 idiom that predates no-cache being universally understood, and the two are now considered equivalent in observable behavior on any compliant cache.
Step 3 — Reproduce both configurations and compare
Set up two test endpoints — one returning no-cache, one returning max-age=0, must-revalidate — and issue back-to-back requests through a shared cache or CDN:
curl -sI https://example.com/no-cache-variant \
| grep -iE '(cache-control|etag|last-modified|age|cf-cache-status)'
Expected response:
HTTP/1.1 200 OK
Cache-Control: no-cache
ETag: "a1b2c3"
Last-Modified: Sun, 05 Jul 2026 18:00:00 GMT
CF-Cache-Status: REVALIDATED
curl -sI https://example.com/max-age-zero-variant \
| grep -iE '(cache-control|etag|last-modified|age|cf-cache-status)'
Expected response:
HTTP/1.1 200 OK
Cache-Control: max-age=0, must-revalidate
ETag: "d4e5f6"
Last-Modified: Sun, 05 Jul 2026 18:00:00 GMT
CF-Cache-Status: REVALIDATED
Both show CF-Cache-Status: REVALIDATED rather than HIT — the CDN stored the response but forwarded a conditional request using If-None-Match before serving it, on every single request, in both cases.
Step 4 — Isolate the disconnected-origin case
The observable difference only appears when the origin is unreachable. Simulate it by blocking the origin and repeating the request against a cache that already holds a stored copy:
# With max-age=0 alone (no must-revalidate) and origin unreachable
curl -sI https://example.com/max-age-zero-no-revalidate | grep -iE '(cache-control|warning|age)'
A cache implementing the disconnected-operation allowance may return:
HTTP/1.1 200 OK
Cache-Control: max-age=0
Age: 312
Warning: 110 - "Response is Stale"
The same test against a no-cache or max-age=0, must-revalidate endpoint should instead produce an error, because revalidation is mandatory with no fallback:
HTTP/1.1 504 Gateway Timeout
Expected Output / Verification
- On a healthy connection to the origin,
no-cacheandmax-age=0, must-revalidateare indistinguishable: both show a conditional request on every fetch and never serve a stored copy without a round trip. - Bare
max-age=0(nomust-revalidate) is the only variant that can legally serve a stale copy with aWarning: 110header when the origin is unreachable. - A
304 Not Modifiedresponse confirms the validator (ETagorLast-Modified) matched and the cache reused the stored body without a full retransfer — this is expected and desirable for all three configurations when the origin is reachable. CF-Cache-Status: REVALIDATED(Cloudflare) orX-Cache: REVALIDATED(generic/Varnish) confirms the edge is storing the object but always checking with origin, rather than serving directly from cache (HIT).
Edge Cases
- HTTP/1.0 intermediaries — some legacy proxies do not parse the
no-cachedirective onCache-Controlat all and instead only understandPragma: no-cachefor requests or numericmax-age. Sendingmax-age=0, must-revalidatealongsideno-cachemaximizes compatibility with caches that predate RFC 9111. - Browser back/forward cache — some browsers treat
no-cacheresponses differently frommax-age=0responses when populating the back/forward navigation cache (bfcache), because bfcache restoration does not always go through the same revalidation path as a normal reload. Test navigation-specific behavior separately from a plain reload. - Missing validators — if the response has neither
ETagnorLast-Modified, revalidation cannot happen conditionally under any of these directives; the cache must perform a full unconditional fetch every time, which is functionally safe but discards the bandwidth savings thatno-cacheis designed to preserve. - CDN-specific interpretation — some CDNs treat
max-age=0withoutmust-revalidateas “do not cache” internally, effectively upgrading it tono-store-like behavior at the edge tier even though RFC 9111 permits storage. Confirm actual edge behavior with your specific CDN’s documentation rather than assuming strict RFC compliance.
Frequently Asked Questions
Is no-cache the same as max-age=0?
Not exactly. no-cache unconditionally requires revalidation before every reuse, regardless of network conditions. max-age=0 alone only marks the response as immediately stale — a cache disconnected from the origin may still serve the stale copy per RFC 9111’s disconnected-operation allowance. Pairing max-age=0 with must-revalidate closes that gap and makes the two effectively equivalent.
Why do some servers send max-age=0, must-revalidate instead of no-cache?
Historically, some older HTTP/1.0 caches did not recognize no-cache on Cache-Control at all, so servers combined max-age=0 with must-revalidate for broader compatibility. On any modern RFC 9111-compliant cache, the two produce the same observable behavior.
Does max-age=0 without must-revalidate ever get served stale?
Yes, in specific circumstances. RFC 9111 permits a cache to serve a stale response if it cannot reach the origin to revalidate, unless must-revalidate or proxy-revalidate is present. no-cache carries no such disconnected-serving allowance, which is the one behavioral gap between the two directives.
Which directive should I use for a resource that must always be revalidated?
Use no-cache. It communicates the intent unambiguously in a single token, and RFC 9111 explicitly favors it over max-age=0, must-revalidate for clarity, even though the two are functionally near-identical on compliant caches.
Related
- When to use
no-storefor sensitive API responses — covers the stricter directive to reach for when revalidation isn’t enough and storage itself must be forbidden. - How
must-revalidateinteracts withmax-age— a deeper look at the directive that closes the disconnected-serving gap described above. - Cache-Control vs Expires: Which wins? — relevant when a response mixes
max-age=0with a legacyExpiresheader. - Public vs private cache scope — determines which cache tiers even see these directives in the first place.