proxy-revalidate

Caching

proxy-revalidate is a Cache-Control response directive: once a shared cache's stored copy is stale, that cache must have the origin validate it before reuse, and may not fall back to the stale copy. It does not apply to private caches, so a disconnected browser may still serve its old copy.

11 min read Updated Aug 30, 2026

Full Explanation

proxy-revalidate is a response directive of the Cache-Control header. It tells a shared cache what to do once its stored copy goes stale. A shared cache can be a CDN edge, a reverse proxy, an ISP proxy, or a corporate proxy. That cache "MUST NOT reuse that response to satisfy another request until it has been successfully validated by the origin" (RFC 9111, section 5.2.2.8). It is the shared-cache half of must-revalidate. RFC 9111 calls the two directives analogous, "except that proxy-revalidate does not apply to private caches". So a browser's own cache is not bound by it.

Three things it is not. It is not a cacheability directive. "proxy-revalidate on its own does not imply that a response is cacheable" (RFC 9111, section 5.2.2.8). So it needs public or an explicit freshness lifetime beside it, or there is nothing to revalidate. It is not a form of no-cache. While the stored copy is fresh, the edge serves it with no origin contact at all. The directive only bites at the moment of expiry. And it is not a blanket licence for the browser. A private cache is free of proxy-revalidate. But RFC 9111 still forbids any cache from generating a stale response "unless it is disconnected or doing so is explicitly permitted by the client or origin server" (RFC 9111, section 4.2.4). The one-line reading is: strict at the edge, tolerant of a browser that has lost the network.

How it works

proxy-revalidate carries no value and sets no lifetime. So it is set beside one: in practice max-age or s-maxage in seconds, or an Expires date. A fresh response is "one whose age has not yet exceeded its freshness lifetime". A stale one "is one where it has" (RFC 9111, section 4.2).

Set beside a lifetime, the header looks like this:

Cache-Control: public, max-age=300, s-maxage=600, proxy-revalidate
  1. While the stored copy is fresh, the edge serves it directly. A fresh response "can be used to satisfy subsequent requests without contacting the origin server, thereby improving efficiency" (RFC 9111, section 4.2). proxy-revalidate changes nothing here.
  2. Once the copy is stale, the directive closes off reuse by a shared cache. That cache must wait until the origin has successfully validated the stored copy (RFC 9111, section 5.2.2.8). A private cache ignores the directive and falls back on its own rules.
  3. The edge validates by forwarding a conditional request. It carries the validators of its stored copy. It MUST send the entity tag from an ETag field, in an If-None-Match header, if the stored response has one. It SHOULD send the Last-Modified value in an If-Modified-Since header. In most cases a cache sends both, "even when entity tags are clearly superior, to allow old intermediaries that do not understand entity tag preconditions to respond appropriately" (RFC 9111, section 4.3.1).
  4. A 304 (Not Modified) means the stored copy is still current. It "can be updated and reused". So the edge freshens its copy and serves it without the body crossing the network again. A full response instead "indicates that none of the stored responses nominated in the conditional request are suitable". The cache then "MUST use the full response to satisfy the request" (RFC 9111, section 4.3.3).
  5. If the origin is unreachable, or answers a 5xx, a cache would normally fall back on serving the old copy. Here that fallback is closed. "A cache MUST NOT generate a stale response if it is prohibited by an explicit in-protocol directive". The RFC names an applicable proxy-revalidate response directive among its examples of exactly that (RFC 9111, section 4.2.4). Note what the spec does not say. It names 504 (Gateway Timeout) as the status a disconnected cache SHOULD generate for must-revalidate (RFC 9111, section 5.2.2.2). But it names no status for proxy-revalidate. So what the edge returns when it cannot validate is left to the implementation.

Net effect: once its copy expires, the edge has exactly one option: ask the origin. The browser is under no such constraint.

Why it matters for a CDN

The directive is addressed at precisely the thing a CDN is. RFC 9111 defines a "shared cache" as "a cache that stores responses for reuse by more than one user; shared caches are usually (but not always) deployed as a part of an intermediary". It defines a "private cache" as one "dedicated to a single user" (RFC 9111, section 1). proxy-revalidate binds the first and says nothing to the second. So it splits one response header into two freshness contracts: one for your edge fleet, one for your visitors.

That split is the whole reason to reach for it. Every edge node holding the resource is forced back to the origin the moment its copy expires. So a correction you publish cannot keep being served by a POP that has not heard about it. Meanwhile, a single reader whose train went into a tunnel may still see the copy their browser already holds. A news article is the classic shape. The edges must converge on the corrected text. One offline reader seeing the pre-correction paragraph is an acceptable cost. Compare the alternatives. must-revalidate would also block that reader's browser. A bare max-age would let both keep serving stale.

The cost is origin traffic, and it is CDN-shaped. Forbidding stale reuse means every POP holding the object must make its own conditional request after expiry. Those requests do not disappear just because most come back 304. Across a large footprint, that is a burst of origin hits every expiry cycle. This is what a tiered topology is for. Akamai's caching documentation points at Tiered Distribution, which "fetches content from other Akamai servers that request content from the origin only when necessary" and so "decreases the load on the origin and reduces the time it takes to retrieve content" (Akamai, Caching behavior). An origin shield does the same job elsewhere.

What CDNs do

Support genuinely varies. It varies by configuration as much as by vendor. Check before relying on it.

  • Cloudflare honours it only with Origin Cache Control on. Its directive table gives two columns. With Origin Cache Control disabled, proxy-revalidate means "Cache directive is ignored and stale is served". With it enabled, it means "Does not serve stale. Must revalidate for CDN but not for browser." Origin Cache Control "is a Cloudflare feature" that makes Cloudflare "strictly respect Cache-Control directives received from the origin server". Free, Pro and Business zones "have this option enabled by default and cannot disable it". Enterprise zones select the behaviour themselves through cache rules or the API (Cloudflare, Origin Cache Control).
  • Akamai honours it through two separate mechanisms in the Caching behavior. With the Honor origin Cache-Control or Honor Cache-Control and Expires options, "if the Cache-Control header contains the must-revalidate or proxy-revalidate directives, edge servers ignore the Force revalidation of stale objects setting and don't serve stale content unless validated with the origin server". No extra opt-in is required. Separately, enabling Enhanced RFC support exposes a granular Honor proxy-revalidate toggle. It reads: "Edge servers won't use stale objects for new requests unless successfully validated with the origin" (Akamai, Caching behavior).
  • Fastly is not steered by it. Its cache-control documentation lists the directives that count: public, private, max-age and s-maxage. It states that "All other Cache-Control directives are ignored and will not influence Fastly's caching, but will be passed through to the browser." The page never names proxy-revalidate. It names must-revalidate among the ignored directives, and proxy-revalidate falls under that catch-all rather than under anything Fastly says about it directly (Fastly, About cache control headers). Read that as not documented as supported. Steer Fastly with s-maxage or a Surrogate-Control header instead.

Watch out for

  • s-maxage already implies it. RFC 9111 says s-maxage "incorporates the semantics of the proxy-revalidate response directive" for a shared cache. It spells out the consequence: "A shared cache MUST NOT reuse a stale response with s-maxage to satisfy another request until it has been successfully validated by the origin" (RFC 9111, section 5.2.2.10). So in Cache-Control: public, max-age=120, s-maxage=600, proxy-revalidate, the trailing directive is doing no work. The edge must already validate at 600 seconds. Cloudflare's table says the same. It renders an s-maxage greater than 1 as "Max-age and proxy-revalidate" (Cloudflare, Origin Cache Control).
  • It no longer unlocks authenticated responses. This is the trap for anyone working from older material. In HTTP/1.1 that was the directive's stated purpose. RFC 2616 described using it "on a response to an authenticated request to permit the user's cache to store and later return the response without needing to revalidate it … while still requiring proxies that service many users to revalidate each time" (RFC 2616, section 14.9.4, obsolete). RFC 9111 dropped it from that role. A shared cache must not reuse a response to a request bearing an Authorization header unless a response directive permits it. Only three directives have that effect: must-revalidate, public and s-maxage (RFC 9111, section 3.5). proxy-revalidate is not one of them. Cloudflare implements exactly this, caching such content "only if must-revalidate, public, or s-maxage is also present" (Cloudflare, Origin Cache Control).
  • It shuts off stale-serving at the edge. The prohibition in section 4.2.4 is unconditional. So an applicable proxy-revalidate overrides the extensions that would otherwise permit staleness. A shared cache cannot serve a stale copy while it revalidates in the background. Cloudflare states that stale-if-error "is ignored … if an explicit in-protocol directive is passed", listing "an applicable s-maxage or proxy-revalidate cache-response-directive" among its examples. It also heads a separate warning that s-maxage disables stale-while-revalidate, because "s-maxage incorporates the semantics of proxy-revalidate, which means a shared cache must not serve stale content without first revalidating with the origin" (Cloudflare, Origin Cache Control). If you want stale-while-revalidate at the edge, send neither this directive nor s-maxage.
  • It does not make anything cacheable. On its own it causes nothing to be stored. So there is nothing stale for it to guard. Pair it with public or an explicit lifetime (RFC 9111, section 5.2.2.8).
  • It frees the browser, not the world. If no visitor may ever see a stale page, this is the wrong directive. A disconnected browser may still serve its copy. must-revalidate is the one that binds every cache, and it is emphatic. It states: "In all circumstances, a cache MUST NOT ignore the must-revalidate directive; in particular, if a cache is disconnected, the cache MUST generate an error response rather than reuse the stale response. The generated status code SHOULD be 504 (Gateway Timeout) unless another error status code is more applicable" (RFC 9111, section 5.2.2.2).

Best practice

  • Choose by where the risk lives. Use proxy-revalidate when the edge fleet must converge, but a stale copy in one offline browser is survivable. Use must-revalidate when a stale read anywhere is a correctness bug. RFC 9111 reserves that directive for cases where "failure to validate a request could cause incorrect operation, such as a silently unexecuted financial transaction" (RFC 9111, section 5.2.2.2).
  • Prefer s-maxage and stop there. It sets the shared-cache lifetime and already carries proxy-revalidate semantics. All three vendor documents cited above show it being honoured. It is one directive instead of two.
  • Always publish a validator. Without an ETag or Last-Modified, the origin cannot answer 304. Every expiry then costs a full body transfer. With a validator, most revalidations are a header exchange instead.
  • Confirm the edge really revalidates rather than serving stale. Watch the Age header reset instead of climbing past your lifetime. Also watch the X-Cache or vendor cache-status value change on the first request after expiry.
  • Test the failure path deliberately, and expect an error. Take the origin down. Confirm the edge does not quietly serve the copy you declared invalid. Remember that a stale extension will not rescue you here, because proxy-revalidate suppresses it. If you would rather have stale content than an error while the origin is down, you have chosen the wrong directive.
  • Do not send it alongside private or no-store. There is no shared copy for it to govern.

Examples

This suits a news site where the CDN must stay exact but the browser may stay stale:

Cache-Control: public, max-age=120, s-maxage=600, proxy-revalidate

CDNs cache for 10 minutes and revalidate when stale. Browsers cache for 2 minutes. They may serve a stale copy when the network is down.

Frequently Asked Questions

proxy-revalidate is a Cache-Control response directive: once a shared cache's stored copy is stale, that cache must have the origin validate it before reuse, and may not fall back to the stale copy. It does not apply to private caches, so a disconnected browser may still serve its old copy.

This suits a news site where the CDN must stay exact but the browser may stay stale:

Cache-Control: public, max-age=120, s-maxage=600, proxy-revalidate

CDNs cache for 10 minutes and revalidate when stale. Browsers cache for 2 minutes. They may serve a stale copy when the network is down.

Related CDN concepts include:

  • must-revalidate — must-revalidate is a Cache-Control response directive (RFC 9111): once a stored response goes stale, any …