Cache-Control

Browser and CDN caching behavior is controlled through the Cache-Control header. Both requests and responses carry directives that determine how browsers, proxies, and CDNs store and serve cached responses across the entire delivery chain.

Usage

Origins, intermediaries, and clients all rely on Cache-Control to agree on when a stored response remains usable and when a fresh copy is needed. A single response often combines several directives to express a complete caching policy: how long to store, who is allowed to store, and what to do once freshness expires.

The table below shows which directives apply to requests, responses, or both.

Directive Request Response
max-age X X
max-stale X
min-fresh X
s-maxage X
no-cache X X
no-store X X
no-transform X X
only-if-cached X
must-revalidate X
proxy-revalidate X
must-understand X
private X
public X
immutable X
stale-while-revalidate X
stale-if-error X X

Multiple directives are separated by commas in a single header value or split across multiple Cache-Control headers. When conflicting directives appear, the most restrictive combination applies.

Request directives

max-age

The max-age directive tells caches to return a stored response only if the response is no older than the specified number of seconds. The value is a non-negative integer. A cache encountering unparseable freshness information, a negative or fractional max-age, treats the response as stale rather than assigning a zero lifetime.

Cache-Control: max-age=<seconds>

Setting max-age=0 forces end-to-end revalidation. This pattern originated in HTTP/1.0 implementations lacking no-cache support.

max-stale

The max-stale directive signals acceptance of a response whose Age has exceeded the freshness lifetime by up to the given number of seconds. The tolerance window starts once max-age expires.

Cache-Control: max-stale=<seconds>

This directive is useful when an origin server is temporarily unreachable and a slightly stale response is acceptable.

min-fresh

The min-fresh directive requests a stored response whose remaining freshness lifetime is at least the specified number of seconds. The cache returns the stored copy only if the response will stay fresh for the additional period.

Cache-Control: min-fresh=<seconds>

no-cache

A request carrying no-cache requires the cache to validate the stored response with the origin before serving the copy. This forces revalidation without discarding the stored entry.

Cache-Control: no-cache

no-store

The no-store directive asks caches not to store the request or the corresponding response.

Cache-Control: no-store

no-transform

The no-transform directive forbids intermediaries from modifying the response body, such as recompressing images or converting media formats. The restriction applies whether the intermediary is a Caching proxy or a forwarding gateway.

Cache-Control: no-transform

only-if-cached

The only-if-cached directive tells the cache to return a stored response without contacting the origin. If no suitable stored response exists, the cache returns a 504 status.

Cache-Control: only-if-cached

Response directives

max-age

The max-age directive declares the number of seconds the response remains fresh after generation. The timer starts from the moment the origin creates the response, so transit time and time spent in intermediate caches count against the budget.

Cache-Control: max-age=<seconds>

A response with max-age=3600 stays fresh for one hour. After the hour elapses, caches treat the response as stale and either revalidate or fetch a new copy depending on other directives present.

s-maxage

The s-maxage (shared max-age) directive overrides max-age for shared caches such as CDNs and proxy servers. Private browser caches ignore s-maxage and fall back to max-age.

Cache-Control: s-maxage=<seconds>

no-cache

The response no-cache directive requires caches to revalidate the stored response with the origin before every reuse. The cache still stores the response, enabling conditional requests with ETag or Last-Modified.

Cache-Control: no-cache

When no-cache includes a list of field names, a cache reuses the rest of the stored response without contacting the origin but omits the listed headers unless the response has been revalidated. The qualified form is defined but not widely implemented, so many caches treat it as a plain no-cache.

Cache-Control: no-cache="Set-Cookie"

no-store

The no-store directive prevents any cache from storing the response. Every subsequent request goes to the origin.

Cache-Control: no-store

no-transform

The response no-transform directive prevents intermediaries from altering the response body before forwarding, whether the intermediary caches the content or not.

Cache-Control: no-transform

must-revalidate

The must-revalidate directive allows caches to serve the response while fresh. Once stale, the cache contacts the origin to revalidate before serving the response again. If the origin is unreachable, the cache returns a 504 instead of serving stale content.

Cache-Control: must-revalidate

proxy-revalidate

The proxy-revalidate directive works identically to must-revalidate, except the requirement applies only to shared caches. Private browser caches are not affected.

Cache-Control: proxy-revalidate

must-understand

The must-understand directive instructs a cache to store the response only when the cache recognizes the status code and understands the associated caching requirements.

Cache-Control: must-understand, no-store

Pairing must-understand with no-store provides a fallback: caches lacking support for must-understand ignore the unknown directive and honor no-store instead.

private

The private directive restricts storage to private caches, typically the end user's browser. Shared caches such as CDNs and proxy servers discard the response.

Cache-Control: private

public

The public directive marks a response as eligible for storage in shared caches. Responses carrying an Authorization header are not stored by shared caches unless the public directive is present.

Cache-Control: public

immutable

The immutable directive guarantees the response body will not change during the freshness lifetime. Caches skip conditional requests for immutable resources, eliminating revalidation round trips for assets like versioned JavaScript bundles or fingerprinted images.

Cache-Control: public, max-age=31536000, immutable

stale-while-revalidate

The stale-while-revalidate directive extends the usability window of a stale response. After freshness expires, caches serve the stale copy while revalidating in the background. The parameter defines how many additional seconds the stale response remains acceptable.

Cache-Control: max-age=600, stale-while-revalidate=30

Once the background revalidation completes, the cache replaces the stale entry with the fresh response. This pattern hides revalidation latency from end users.

stale-if-error

The stale-if-error directive allows caches to serve a stale response when the origin returns an error status (500, 502, 503, or 504) or is unreachable. The parameter sets the number of seconds beyond the freshness lifetime during which the stale response remains usable as a fallback.

Cache-Control: max-age=600, stale-if-error=86400

Shared caches and CDNs

CDN edges are shared caches, and each vendor layers configuration on top of the directives. Cloudflare documents the sharpest divergences, and the same questions apply at any edge provider.

How strictly the edge follows the origin header depends on Cloudflare's Origin Cache Control setting. With the setting on, no-cache and max-age=0 mean store-and-revalidate. With the setting off, both keep the response out of the edge cache entirely. no-store stays uncached either way, and immutable only changes browser behavior, never the shared cache.

Cloudflare documents directive values as unquoted integers, so a fractional value like max-age=2.5 falls outside the accepted form and the freshness lifetime the origin intended cannot be relied on. s-maxage also carries an implicit proxy-revalidate at shared caches, which turns off stale-while-revalidate for the edge.

Dedicated targeting headers override the general one. When CDN-Cache-Control or a vendor variant is present, the edge follows the dedicated header while Cache-Control passes through untouched for browsers. A Surrogate-Control header makes Cloudflare ignore Cache-Control completely, even when empty.

Edge configuration wins over both. A Cloudflare Cache Rule sets an Edge TTL mode of respect origin, bypass by default, or override origin, and an override never appears in response headers. A response reporting the origin's max-age while the edge holds a different TTL is expected behavior, so the CF-Cache-Status header, not the Cache-Control value, tells the caching story at the edge.

Default cacheability at Cloudflare follows the file extension, not the media type. HTML and JSON stay uncached by default and report DYNAMIC in CF-Cache-Status, while robots.txt is cached. Sending strong Cache-Control headers on HTML only reaches the edge cache once a Cache Rule makes the content eligible.

Example

A static asset served with a long freshness lifetime and the immutable flag. The browser and any shared cache store this response for one year without revalidation.

Cache-Control: public, max-age=31536000, immutable

An API response intended only for the requesting browser. The cache stores the response for five minutes and revalidates before reuse once stale.

Cache-Control: private, max-age=300, must-revalidate

A CDN-targeted policy giving the shared cache a ten-minute window while the browser cache gets one minute. Stale responses are served for up to 30 seconds while the CDN revalidates in the background.

Cache-Control: s-maxage=600, max-age=60, stale-while-revalidate=30

Crawlers and Cache-Control

Google's crawling infrastructure implements heuristic HTTP caching. The max-age directive helps Googlebot determine recrawl frequency, and a page with a long max-age is re-fetched less often. Google's crawl caching otherwise relies on ETag and Last-Modified validators, and other caching directives carry no crawl signal.

In browsers and CDNs, responses without any cache headers are heuristically cacheable by default for certain status codes, so origins wanting to prevent caching there send no-store explicitly. Bingbot relies on crawl-delay in robots.txt and IndexNow for crawl pacing.

Troubleshooting

Unexpected caching behavior stems from directive misunderstandings, CDN overrides, or conflicting headers.

  1. Stale content served despite no-cache directive. The no-cache directive does not prevent storage. Caches store the response and revalidate on every reuse. To prevent storage entirely, use no-store on its own, which already stops a cache from keeping the response.

  2. CDN ignores Cache-Control directives. Many CDNs override origin Cache-Control with their own policies. Cloudflare respects origin headers by default but overrides them when a Cache Rule, or a legacy Page Rule, is active. AWS CloudFront uses the origin header unless a cache policy with custom TTLs is attached. Check the CDN dashboard for override rules before debugging the origin configuration.

  3. Stale content persists after lowering max-age. CDN edge nodes and browser caches retain responses until the original max-age expires. Lowering the value on the origin does not purge already-cached copies. Issue a CDN purge after changing cache lifetimes. In Cloudflare, purge by URL or use a full zone purge. In CloudFront, create an invalidation for the affected paths.

  4. Browser shows old page on back/forward navigation despite no-store. The back/forward cache (bfcache) in Chrome, Firefox, and Safari restores the full page state from memory, bypassing no-store. This behavior is by design and does not represent a cache violation. Inspect the Cache-Control header in DevTools Network tab with "Disable cache" unchecked to confirm the header reaches the browser. Adding unload event listeners disables bfcache in some browsers but degrades performance.

  5. Expires header conflicts with Cache-Control. When both headers are present, Cache-Control: max-age takes precedence over Expires. Remove the Expires header to avoid confusion. In nginx:

    expires off;
    add_header Cache-Control "max-age=3600";
    

    In Apache:

    Header unset Expires
    Header set Cache-Control "max-age=3600"
    
  6. Diagnosing cache headers with curl and DevTools. Run curl -sS -o /dev/null -D- https://example.re to inspect response headers. Major CDNs, including Cloudflare and CloudFront, ignore the request no-cache directive by default, so sending -H "Cache-Control: no-cache" does not bypass their caches. Reach the origin directly with a unique query string or by requesting the origin hostname. In browser DevTools, open the Network tab, select the request, and check the Response Headers section. Look for Age, X-Cache, and CF-Cache-Status headers to determine whether the response came from a CDN edge node or the origin.

See also

Last updated: August 18, 2026