Skip to main content
sitewarm

Cache-Control for HTML pages behind a CDN

· sitewarm

An HTML page is the response most likely to change on a deploy and the one a CDN is most often asked to cache. Cache-Control is where the origin says how long a copy may be reused, and by whom. This article explains what max-age, s-maxage, stale-while-revalidate, stale-if-error and immutable mean according to the standards, how they combine on a page, and what the standards leave to each cache.

Everything here comes from RFC 9111, RFC 5861, RFC 8246 and MDN, read on 2026-10-24. It does not cover which CDN honors which directive: check your CDN's documentation for that. The example headers are illustrative, not captured from a real site, and are not a recommendation.

Two kinds of cache, two lifetimes

A response can be stored by a private cache, such as a browser, and by a shared cache, such as a CDN. RFC 9111 gives each directive a scope, and the difference matters for HTML because you often want the two to hold a page for different lengths of time.

  • `max-age`: MDN says the response "remains fresh until N seconds after the response is generated", and RFC 9111 says it is stale once its age is greater than that number of seconds. MDN adds that it counts from when the origin generated the response, not from when the cache received it.
  • `s-maxage`: MDN says it indicates how long the response remains fresh in a shared cache, that private caches ignore it, and that it overrides max-age and Expires for shared caches.

When more than one is present, RFC 9111 section 4.2.1 gives the order a cache follows to find the freshness lifetime: s-maxage if the cache is shared, then max-age, then Expires. So a page with both max-age=0 and s-maxage=300 is fresh for 300 seconds in a CDN and for 0 seconds in a cache that uses max-age.

no-cache and no-store are not the same

MDN is direct about this: no-cache "does not mean 'don't cache'". The response can be stored, but "must be validated with the origin server before each reuse". RFC 9111 words it as: the response must not be used to satisfy another request without being forwarded for validation. If what you want is "do not store", MDN says no-store is the directive: it tells caches of any kind, private or shared, not to store the response.

MDN's own example for an HTML file is Cache-Control: no-cache, to make sure the client receives a new version of the HTML and its static assets. See `no-cache` and `no-store`.

`public` and `private` are about where a copy may live. RFC 9111 says public indicates that a cache may store the response even if it would otherwise be prohibited, and that an unqualified private means a shared cache must not store it. A page meant for one user belongs in the second group.

Serving a stale copy on purpose

A copy is stale when its age passes its lifetime. By default, RFC 9111 section 4.2.4 says a cache must not generate a stale response unless it is disconnected, or unless an in-protocol directive allows it, and must not do so if a directive prohibits it. RFC 5861 defines two extensions that allow it:

  • `stale-while-revalidate`: RFC 5861 says caches "MAY serve the response in which it appears after it becomes stale, up to the indicated number of seconds." Its example is Cache-Control: max-age=600, stale-while-revalidate=30. MDN describes it as reusing a stale response while it is revalidated in the background.
  • `stale-if-error`: RFC 5861 says that when an error occurs, a cached stale response may be used regardless of other freshness information, and that the value is the upper limit to staleness. MDN lists the errors as responses with status 500, 502, 503 or 504, or an error generated locally. The RFC's example is Cache-Control: max-age=600, stale-if-error=1200.

Both are permissions, written with "MAY". A cache is allowed to use them, and the standards do not require every cache to. `must-revalidate` goes the other way: RFC 9111 says that once the response is stale, a cache must not reuse it until it has been successfully validated by the origin, and must not ignore the directive.

immutable and HTML

RFC 8246 defines `immutable` to say that the origin server will not update the representation of the resource during the freshness lifetime of the response. Clients should not send a conditional request during that lifetime. The RFC's motivating case is versioned sub-resources, for which a very large freshness lifetime is safe.

The RFC has no rule for HTML. Read against its own definition, though, immutable is a promise that the response will not change while it is fresh, and a page that is edited or redeployed during its lifetime does not meet that promise. Whether to send it on a page is a decision about that promise. RFC 8246 also says the extension applies only during the freshness lifetime, and that stale responses should be revalidated as they normally would be.

Reading a combination

This header is illustrative. It is written to show the reading, not captured from a real site, and it is not a recommendation for any site.

Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=60
Age of the copy in a shared cacheWhat the standards allow
0 to 300 secondsFresh. s-maxage is the lifetime that applies to a shared cache, and it overrides max-age.
300 to 360 secondsStale, but within stale-while-revalidate=60. The cache may serve it while it revalidates.
Over 360 secondsStale and outside the window. The cache has to get a fresh response or validate the copy before reuse.

A private cache that uses max-age=0 treats the same response as stale immediately. One header, two readings, and neither is an error.

The trade-off is written in the arithmetic. A longer lifetime means fewer requests to the origin, and it also means a changed page can be served until the copy expires or is purged. The standards describe the mechanism and leave the choice of numbers to you.

What these directives do not tell you

  • They are instructions, not reports. Whether a CDN stored or served a copy is shown by its status headers, as in how to read Age, Cache-Control and X-Cache.
  • A CDN may support only some of them, or add its own rules and overrides. Read its documentation before relying on a directive.
  • Other headers and settings can change the outcome, including cookies, Vary and cache rules configured at the CDN.
  • None of them says anything about how fast a page loads or where it appears in search results.

Where sitewarm fits

sitewarm requests pages from your sitemap, up to the scan's URL limit, and reports the cache state the CDN returns for each one, with the vantage point it was measured from.

Frequently asked questions

What is the difference between max-age and s-maxage?

max-age sets how long a response stays fresh. s-maxage sets it for shared caches such as CDNs only, is ignored by private caches, and overrides max-age and Expires there. See s-maxage.

Does no-cache mean the page is not cached?

No. MDN says no-cache allows a cache to store the response but requires it to be validated with the origin before each reuse. no-store is the directive that tells caches not to store it.

What does stale-while-revalidate do?

RFC 5861 says caches may serve the response after it becomes stale, up to the indicated number of seconds. MDN describes it as reusing the stale response while it is revalidated in the background. It is a permission, so whether a given CDN uses it depends on that CDN. See stale-while-revalidate.

Should an HTML page be immutable?

RFC 8246 defines immutable as the origin promising not to update the representation during the freshness lifetime. Its motivating case is versioned sub-resources. A page that changes during its lifetime does not match that definition.

References

  1. RFC 9111: HTTP Cachinghttps://www.rfc-editor.org/rfc/rfc9111
  2. RFC 5861: HTTP Cache-Control Extensions for Stale Contenthttps://www.rfc-editor.org/rfc/rfc5861
  3. RFC 8246: HTTP Immutable Responseshttps://www.rfc-editor.org/rfc/rfc8246
  4. MDN: Cache-Controlhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control

See the cache state your CDN returns for each page

sitewarm requests pages from your sitemap, up to the scan's URL limit, after a deploy and reports the cache state the CDN returns for each one, with the vantage point it was measured from.

Start warming