proxy-revalidate vs must-revalidate: Which to Send
Problem Statement
Two directives express the same rule — an expired response must not be reused without revalidation — and differ only in which caches they bind. Choosing between them is a question about where staleness is actually harmful, and getting it wrong either leaves a shared cache free to serve old data or makes every browser dependent on origin availability for no benefit.
Prerequisite Concepts
- must-revalidate and proxy-revalidate explained — the mechanism, the
504requirement, and why both are inert while a response is fresh. - Understanding HTTP cache hierarchy — the private/shared split the two directives are defined against.
- Header stacking and directive precedence — how these interact with the lifetime directives they accompany.
Step-by-Step Resolution
Step 1 — Ask the question separately for each tier
The decision is not “is staleness acceptable” but “is staleness acceptable here”. A shared cache serving a stale response affects every user in a region simultaneously; a browser serving one affects one person who can usually resolve it by reloading.
Step 2 — Send the narrower directive where it is sufficient
For content that many users share, where the edge must always hold verified data but a single browser falling back to its own copy is harmless:
Cache-Control: public, max-age=30, s-maxage=120, proxy-revalidate
ETag: "frag-2f81"
For content where a stale value could cause a wrong decision regardless of who sees it:
Cache-Control: private, max-age=15, must-revalidate
ETag: "acct-9471-r8823"
Step 3 — Verify the shared tier
This is the only test that exercises the directive, and it requires the origin to be unreachable while a stored entry is expired. In staging:
# Warm the entry
curl -sI https://staging.example.com/fragments/summary | grep -iE '^age|cache-control'
# Wait past s-maxage, stop the origin, then request again
sleep 130
curl -s -o /dev/null -w '%{http_code}\n' https://staging.example.com/fragments/summary
# Expected with proxy-revalidate or must-revalidate: 504
# Expected without either: 200, served stale
A 200 where you expected 504 means something is overriding the directive — a vendor grace period, an always-online feature, or a proxy that does not implement it.
Step 4 — Verify the browser tier
The two directives diverge here, and this is the test that proves you chose the one you meant. With a stored, expired entry in a browser profile and the origin unreachable:
- Under
must-revalidate, the browser must not reuse the entry and the navigation fails. - Under
proxy-revalidate, the browser is free to serve its own copy and the page renders from cache.
Step 5 — Record the availability consequence
Whichever directive you send, the route now has an availability ceiling set by the origin. That belongs in the same dashboard as the origin’s error rate, because the failure mode is specific and easy to miss: overall site availability stays healthy while a small number of strict routes return 504 during an origin incident.
The practical form is a per-route error-rate panel that separates 504 responses generated by the cache from 5xx responses proxied from the origin. The two look identical in a total error count and mean very different things — the first is the directive working as designed, the second is the origin failing. Teams that do not separate them tend to discover the distinction during an incident, when the difference between “the cache is refusing to serve stale data” and “the application is broken” matters most.
It is also worth reviewing which routes carry the directive on a schedule. Strictness accumulates: a route marked strict during an incident three quarters ago is rarely revisited, and each one adds an origin dependency that was justified once and may not be now.
Expected Output / Verification
A correct configuration produces three consistent observations: free reuse for the whole lifetime, an error rather than stale content at the shared tier once expired and the origin unavailable, and browser behaviour that matches the directive you chose rather than the one you assumed.
# Within the lifetime — reuse, no origin contact
HTTP/2 200
Age: 74
Cache-Control: public, max-age=30, s-maxage=120, proxy-revalidate
# Expired, origin unreachable — the shared tier refuses to serve stale
HTTP/2 504
Edge Cases
- An intermediary that ignores
proxy-revalidate. Support is thinner than formust-revalidate. If a tier ignores it, that tier silently retains its discretion to serve stale, which is precisely the outcome the directive was chosen to prevent. - A vendor grace period overriding both. Fastly’s
beresp.graceand equivalent settings elsewhere authorise stale serving independently of the response header. Set grace to zero for strict routes rather than assuming the directive wins. proxy-revalidateon aprivateresponse. The combination is inert: aprivateresponse is never stored by a shared cache, so a directive constraining shared caches has nothing to act on.- Choosing
must-revalidatefor a route with no validator. Every post-expiry request becomes a full refetch rather than a304, turning a cheap correctness guarantee into a bandwidth cost that scales with traffic. - Forgetting that both are inert while fresh. A team adds the directive expecting immediate revalidation, observes no change, and concludes it is unsupported — when in fact the lifetime simply had not elapsed.
Frequently Asked Questions
Is proxy-revalidate widely supported?
Support in CDNs and reverse proxies is good, but it is far less commonly implemented than must-revalidate and some intermediaries ignore it. Verify against the specific tiers in your path rather than assuming, and treat an unverified directive as advisory.
Can I send both directives together?
You can, and it is harmless but redundant: must-revalidate already binds shared caches, so adding proxy-revalidate changes nothing. Sending both usually indicates uncertainty about which one was needed.
Does proxy-revalidate stop the edge storing the response?
No. It only removes the edge’s discretion to serve an expired copy. If the response must not be stored at the shared tier at all, that is private, and if it must not be stored anywhere it is no-store.
Which one should an authenticated API use?
Usually must-revalidate together with private, because a stale authenticated response is a correctness problem in the browser as much as at the edge. proxy-revalidate suits shared content where a single user’s brief staleness is harmless.
Related
- Public vs private cache scope covers the storage-scope decision these directives sit alongside.
- Serving stale content with stale-while-revalidate is the directive family these two directly contradict.
- Cache debugging and observability shows how to confirm from headers which branch a cache actually took.