Cache-Control
Cache-Control is the HTTP header field, defined in RFC 9111, that carries caching directives to browsers, proxies and CDNs: whether a response may be stored, by which caches, how long it stays fresh, and what a cache must do once it goes stale.
Full Explanation
Cache-Control is the HTTP header field that carries caching directives. It reaches every cache on the path between a client and an origin server: a browser's private cache, a corporate proxy, or a shared cache such as a CDN. It states whether a response may be stored, and by which caches. It also states how long a response counts as fresh, and what a cache must do once it goes stale. It is defined in RFC 9111, section 5.2 (STD 98, June 2022). This RFC obsoletes RFC 7234. The older RFC 7234 and RFC 2616 references are superseded. They are still common in vendor documentation.
What it is not: Cache-Control is not a security or privacy control. It is not a routing control, and not a guarantee. It expresses intent that compliant caches honour. RFC 9111 states that no-store "is not a reliable or sufficient mechanism for ensuring privacy" (section 5.2.2.5). It also cannot be aimed at one particular cache. A proxy "MUST pass cache directives through in forwarded messages", and "it is not possible to target a directive to a specific cache" (section 5.2).
The helicopter view: use max-age for every cache, s-maxage to give shared caches a different lifetime, and private or no-store for anything per-user. Use the stale-* extensions for resilience, and immutable on versioned assets. Then check what your CDN actually returns, because a CDN both clamps the values it applies and rewrites the header it sends downstream.
How it works
The field value is a comma-separated list of directives. Each directive is compared case-insensitively. Each one can optionally take an argument, such as max-age=600. The same field name is used in both directions, but the two directions are not symmetrical. Response directives bind caches: "A cache MUST obey the Cache-Control directives defined in this section" (section 5.2.2). Request directives such as no-cache or max-stale "are advisory; caches MAY implement them, but are not required to" (section 5.2.1). Directives are also unidirectional: one present in a request is not copied into the response.
Where a response may be stored. public explicitly marks a response as cacheable, even when it would otherwise be prohibited, subject to the storage constraints of section 3. private means a shared cache "MUST NOT store the response". A private cache still may store it. no-cache permits storage, but forbids reuse "without forwarding it for validation and receiving a successful response". Only no-store forbids storage outright, in private and shared caches alike.
How long it stays fresh. max-age gives a freshness lifetime in seconds to every cache. s-maxage overrides it for shared caches only, so it has no effect on a private cache such as a browser. A shared cache evaluates s-maxage first, then max-age, then Expires (section 4.2.1). When max-age or s-maxage is present, the recipient "MUST ignore the Expires header field" (section 5.3). The HTTP/1.0 Pragma request header is deprecated by the same specification (section 5.4). The Age header reports how much of that lifetime a cached copy has already used (section 5.1).
What happens once it is stale. A stale response is not automatically discarded. The cache decides whether to serve it or revalidate. must-revalidate forbids reuse "until it has been successfully validated by the origin". proxy-revalidate imposes the same rule, but only on shared caches. Revalidation means a conditional request that carries a validator. This is typically an ETag in If-None-Match (section 4.3.1). A 304 (Not Modified) response then updates the stored copy in place, rather than resending the body (section 4.3.4).
Two extensions trade freshness for availability. Each is bounded by the seconds you allow. stale-while-revalidate lets a cache serve the stale response "up to the indicated number of seconds" while it revalidates in the background. stale-if-error lets it serve stale content when an error is encountered, "regardless of other freshness information" (RFC 5861, sections 3 and 4). Both are permissions, not obligations. Both are cancelled by any directive that prohibits a stale response: no-cache, must-revalidate, or an applicable s-maxage or proxy-revalidate (section 4.2.4). Separately, immutable tells clients not to issue a conditional request during the freshness lifetime, even on a reload (RFC 8246, section 2). no-transform forbids an intermediary from transforming the content.
If no directive sets an expiration, a cache "MAY assign a heuristic expiration time", using signals such as Last-Modified (section 4.2.2). The specification imposes only a ceiling. This ceiling is no more than some fraction of the interval since Last-Modified, with 10% named as a typical setting of that fraction. Heuristics may be applied only to responses whose status code is heuristically cacheable, or that are explicitly marked cacheable.
Why it matters for a CDN
A CDN is a shared cache sitting in the middle of the delivery path, so Cache-Control is the contract that decides your cache hit ratio and the request volume your origin has to absorb. If the lifetime is too short, every edge revalidates constantly. This turns the CDN into a thin proxy in front of an overloaded origin. If the lifetime is too long, one bad object is served to every user until it expires or you purge it.
The split between shared and private caches is what makes the header a CDN lever, rather than a browser setting. s-maxage=3600, max-age=60 lets the edge hold a copy for an hour, while browsers recheck every minute. A change then reaches users a minute after you purge the edge. The limit is that one header reaches both audiences. RFC 9111 offers no way to address a single cache. CDNs answer that with their own header names, which sit above Cache-Control in their precedence. Fastly reads Surrogate-Control before any Cache-Control directive. Fastly documents it as the way to set "longer cache times for Fastly while maintaining shorter times for browsers" (Fastly). Cloudflare treats Cloudflare-Cdn-Cache-Control as forcing Origin Cache Control on (Cloudflare).
What CDNs do
- Cloudflare: Origin Cache Control makes Cloudflare "strictly respect
Cache-Controldirectives received from the origin server". It is on by default for Free, Pro and Business customers, who cannot disable it. Enterprise customers choose per website. With it enabled,max-age=0,s-maxage=0andno-cacheare all cached and always revalidated, rather than bypassed.no-storeis never cached. Edge Cache TTL rules overrides-maxage. Browser Cache TTL rules override themax-agesent downstream (Origin Cache Control). - AWS CloudFront: the origin's
max-age(ors-maxage, which takes priority at the edge when both are present) is clamped by the cache policy. With Minimum TTL 0, CloudFront caches for the lesser of the directive and the Maximum TTL. With Minimum TTL above 0, it also raises anything shorter up to that floor. Default TTL applies only when the origin sends no directive at all. Critically, "if your minimum TTL is greater than 0, CloudFront uses the cache policy's minimum TTL, even if theCache-Control: no-cache,no-store, and/orprivatedirectives are present in the origin headers". CloudFront supportsstale-while-revalidateandstale-if-error, but it serves stale for the lesser of the directive and the Maximum TTL. It ignores Cache-Control in viewer requests (Manage how long content stays in the cache). - Fastly: TTL comes from the first of
Surrogate-Control,Cache-Control: s-maxage,Cache-Control: max-age, thenExpires. Fastly's documented practice is to "configure appropriateCache-Controlheaders on all responses from your origin servers instead of relying on fallback TTLs". Fastly also recommends usingSurrogate-Controlfor split edge/browser policies, removingCache-Control: privatefrom anything you want cached, and preferring purging over short lifetimes. It supportsstale-while-revalidateandstale-if-errorin Cache-Control, or the equivalent VCL variables (caching configuration best practices).
Watch out for
no-cachedoes not mean "do not cache". The response may be stored. It just may not be reused without a successful validation. Onlyno-storeprevents storage.- No Cache-Control header does not mean no caching. Heuristic freshness applies whenever no explicit expiration is present and the response is heuristically cacheable, so an unmarked response can be held for a while by any cache in the chain (section 4.2.2).
- Conflict resolution is guidance, not a hard rule. RFC 9111 says that where directives conflict, "e.g., both max-age and no-cache are present", "the most restrictive directive should be honored". It also says that caches "are encouraged to consider" responses with invalid freshness information, such as a non-integer
max-age, to be stale (section 4.2.1). Implementations differ. Cloudflare, for one, ignores floating-point TTL values such asmax-age=2.5, "potentially causing cache bypass" (Cloudflare). Do not rely on a malformed or contradictory header doing what you intended. s-maxagesilently disablesstale-while-revalidateat a shared cache. This is because it carries the semantics ofproxy-revalidate, and a stale response is then prohibited by an in-protocol directive (section 4.2.4). Cloudflare documents the same conclusion, and advises against combining them (Cloudflare).no-storeis not a privacy mechanism. A malicious or compromised cache may ignore it, and the network path may be observable. Protect sensitive data with authentication and transport security, not a header.- A CDN may not honour or forward your header unchanged. It can clamp the TTL it applies, keep
no-storeorprivatecontent for a configured minimum TTL, and rewrite themax-ageit sends to browsers. Test the response the browser actually receives, not the one your origin emits. - Directives you send to the edge also reach the browser. Sending
no-storeto fix a CDN caching bug also disables the browser cache. In many browsers, it disables the back/forward cache too (Cloudflare). Reach for a CDN-specific control header when only the edge should change behaviour.
Best practice
- Set an explicit Cache-Control on every response. Heuristic freshness and vendor fallback TTLs are what you get when you do not decide.
- For fingerprinted static assets, publish under a new URL on every change. Serve a very long lifetime with
immutable. RFC 8246 givesCache-Control: max-age=31536000, immutableas its example. Addingpublicmakes the intent explicit (RFC 8246, section 2.2). - Mark anything per-user
private. Useno-storewhen it must not be written down at all. Where a response genuinely varies by request header, set Vary as well, since a cache "MUST NOT use that stored response without revalidation" unless the nominated fields match (section 4.1). - Use
s-maxage(or the CDN's own control header) to cache longer at the edge than in browsers. Then purge when content changes, instead of paying for a shortmax-ageon every request. - Add
stale-while-revalidateandstale-if-errorfor resilience. Do not add them next tos-maxageormust-revalidate, which prohibit stale responses. Use a longmax-ageplus purging, or the CDN's own stale settings, when you need both. - Verify at the edge and in the browser. Read
Age, the CDN's cache-status header, and theCache-Controlactually returned. A header your CDN silently overrode is a header you do not have.
Interactive Animation
Examples
# Common patterns:
# Static assets (immutable, hash in filename)
Cache-Control: public, max-age=31536000, immutable
# API responses (short browser cache, longer CDN cache)
Cache-Control: public, max-age=60, s-maxage=3600
# Personalized content (don't cache on CDN)
Cache-Control: private, max-age=0, no-store
# Resilient caching (serve stale on errors)
Cache-Control: public, max-age=300, stale-while-revalidate=60, stale-if-error=86400
# Nginx example
location /api/ {
add_header Cache-Control "public, max-age=60, s-maxage=3600";
}
Frequently Asked Questions
Cache-Control is the HTTP header field, defined in RFC 9111, that carries caching directives to browsers, proxies and CDNs: whether a response may be stored, by which caches, how long it stays fresh, and what a cache must do once it goes stale.
# Common patterns:
# Static assets (immutable, hash in filename)
Cache-Control: public, max-age=31536000, immutable
# API responses (short browser cache, longer CDN cache)
Cache-Control: public, max-age=60, s-maxage=3600
# Personalized content (don't cache on CDN)
Cache-Control: private, max-age=0, no-store
# Resilient caching (serve stale on errors)
Cache-Control: public, max-age=300, stale-while-revalidate=60, stale-if-error=86400
# Nginx example
location /api/ {
add_header Cache-Control "public, max-age=60, s-maxage=3600";
}
Related CDN concepts include:
- Age Header — The Age response header is a cache's estimate, in seconds, of how long ago the …
- Vary Header — Vary is the response header that names the request headers the origin used to select …
- max-age — max-age is a Cache-Control response directive giving the seconds a stored response may be reused …
- must-revalidate — must-revalidate is a Cache-Control response directive (RFC 9111): once a stored response goes stale, any …
- proxy-revalidate — proxy-revalidate is a Cache-Control response directive: once a shared cache's stored copy is stale, that …
- s-maxage — A Cache-Control response directive that sets how long a shared cache — a CDN edge, …
- stale-if-error — stale-if-error is a Cache-Control extension directive (RFC 5861) that lets a cache serve an already-stored …
- stale-while-revalidate — A Cache-Control response directive (RFC 5861) that lets a cache keep serving a stale stored …