How must-revalidate Interacts with max-age

max-age sets how long a response is considered fresh, but it says nothing about what a cache is allowed to do once that window closes. Left alone, most caches treat expiry as a soft boundary: they attempt revalidation, but if the origin is slow or unreachable, they often fall back to serving the stale copy rather than failing the request. Adding must-revalidate removes that fallback entirely. It does not change when a response becomes stale — it changes what happens the instant it does, converting a soft, forgiving expiry into a hard one.

Prerequisite Concepts

Before working through the interaction, make sure you’re familiar with the surrounding directives:

Step-by-Step Resolution

Step 1 — Serve a baseline response with max-age only

Configure the origin to return a short freshness lifetime with no revalidation directive:

HTTP/1.1 200 OK
Cache-Control: max-age=5
ETag: "v1-abc123"
Date: Mon, 06 Jul 2026 12:00:00 GMT

Request it once to populate the cache, then wait past the 5-second window:

curl -sI https://example.com/report.json | grep -iE '(cache-control|age|date)'
sleep 6

At this point the stored copy is formally stale, but nothing has been discarded — it’s still sitting in the cache, and RFC 9111 §4.2.4 permits (without prohibiting) a shared cache to keep it around and consider serving it if revalidation cannot complete.

Step 2 — Simulate an origin outage on the stale response

With the response now stale, force the origin into a failure state — stop the upstream process, or point the CDN at an unreachable backend — and repeat the request through the shared cache:

curl -s -o /dev/null -w '%{http_code}\n' https://example.com/report.json

Many production caches (and CDNs configured with a “serve stale on error” policy) return the old 200 OK body with Age continuing to climb, rather than propagating the origin failure to the client. This is the default, forgiving behavior that plain max-age allows but does not mandate — it’s implementation-dependent, which is exactly the ambiguity must-revalidate closes.

Step 3 — Add must-revalidate and repeat the outage test

Change the origin’s response header to:

HTTP/1.1 200 OK
Cache-Control: max-age=5, must-revalidate
ETag: "v1-abc123"
Date: Mon, 06 Jul 2026 12:00:10 GMT

Repeat steps 1 and 2 exactly. Populate the cache, wait past the 5-second window, then take the origin offline and request again:

curl -s -o /dev/null -w '%{http_code}\n' https://example.com/report.json

This time the cache must not return the stale body. Per RFC 9111 §5.2.2.2, a stale response marked must-revalidate must not be used to satisfy a subsequent request without successful validation from the origin. Since validation cannot succeed, the correct response is an error — typically surfaced by a reverse proxy or CDN as:

HTTP/1.1 504 Gateway Timeout
Content-Type: text/plain

The stale-serving fallback that step 2 demonstrated is gone. The client sees the failure instead of quietly outdated data.

Step 4 — Confirm must-revalidate overrides stale-while-revalidate

A common assumption is that stacking stale-while-revalidate alongside must-revalidate will still grant a grace window. Test it:

HTTP/1.1 200 OK
Cache-Control: max-age=5, must-revalidate, stale-while-revalidate=30
ETag: "v1-abc123"

Repeat the same expire-then-fail sequence from Step 3. Even though stale-while-revalidate=30 nominally permits serving stale content for 30 seconds after expiry while a background revalidation runs, RFC 9111 is explicit that a cache must not apply stale-while-revalidate to a response carrying must-revalidate. The hard-failure behavior from Step 3 persists — the 504 still appears, confirming must-revalidate wins the conflict.

Expected Output / Verification

  • Before must-revalidate, a stale response under origin failure returns 200 OK with an incrementing Age and an unchanged ETag — the cache served its last-known-good copy.
  • After adding must-revalidate, the same failure sequence returns a 504 Gateway Timeout (or an equivalent 5xx from your specific proxy/CDN) instead of the stale body.
  • Adding stale-while-revalidate alongside must-revalidate produces no observable change from Step 3 — the grace window is suppressed. If your cache instead serves stale content in this configuration, that cache is not RFC 9111-compliant for this directive combination.
  • Within the freshness window (before max-age elapses), all three configurations behave identically — must-revalidate has no effect on fresh responses.

Edge Cases

  • Browser caches vs shared cachesmust-revalidate applies to private (browser) caches too, unlike proxy-revalidate, which only binds shared caches. A browser tab reloading a stale, must-revalidate-tagged resource over a flaky connection can surface a hard network error rather than a stale render.
  • HTTP/1.1 vs HTTP/2 — the revalidation semantics are identical across protocol versions; only the transport for the conditional request changes.
  • Missing validators — if the response has no ETag or Last-Modified, the cache cannot issue a conditional request at all. With must-revalidate present and validators absent, an origin outage guarantees an error response, since a full unconditional re-fetch is the only remaining option and it will also fail.
  • Interaction with no-cacheno-cache forces revalidation on every request regardless of freshness, making must-revalidate redundant on responses that already carry it; the two are commonly confused but solve different problems.

Frequently Asked Questions

Does must-revalidate do anything before max-age expires?

No. It is inert while the response is fresh. It only activates once the freshness lifetime set by max-age (or s-maxage) has elapsed and the stored response becomes stale.

Does must-revalidate override stale-while-revalidate?

Yes. RFC 9111 states a cache must not apply the stale-while-revalidate grace window to a response that also carries must-revalidate. The hard-revalidation rule takes precedence.

What does a cache do if the origin is unreachable during revalidation?

Without must-revalidate, a cache is permitted to serve the stale response rather than fail. With must-revalidate present, the cache must not serve the stale copy and instead returns an error, typically a 504 Gateway Timeout.

Is must-revalidate the same as proxy-revalidate?

They enforce the same rule on different cache scopes. must-revalidate applies to all caches, including private browser caches. proxy-revalidate applies only to shared caches, leaving private caches free to serve stale.


Back to Header Stacking and Directive Precedence