The Complete Guide to the Cache-Control Header
Understand with AI
Discuss with your preferred AI assistant
A correctly cached asset is served from the browser disk cache instantly, with no network round-trip at all.
Fingerprinted JS, CSS, and fonts can safely use max-age=31536000, immutable — the URL changes on every deploy.
A long CDN s-maxage plus stale-while-revalidate can absorb the vast majority of traffic before it reaches your server.
The Cache-Control header is the single most powerful lever you have over web performance. Get it right and repeat visitors load your pages from disk in milliseconds, your CDN absorbs traffic spikes, and your origin server barely breaks a sweat. Get it wrong and you either serve users stale content for days or hammer your server with requests that should never have left the browser.
This guide explains exactly what each Cache-Control directive does, the values you should use for different kinds of assets, and the mistakes that quietly slow sites down. Pair it with the generator above to produce a copy-paste header in seconds.
What Is the Cache-Control Header?
Cache-Control is an HTTP response header that tells browsers and shared caches (CDNs and proxies) whether they may store a response, how long it stays fresh, and what to do once it goes stale. It is defined by RFC 7234 and is supported by every modern browser and CDN.
A typical value looks like public, max-age=31536000, immutable. Each comma-separated token is a directive, and together they form the caching policy for that response.
The Core Directives, Explained
Most real-world headers are built from a small set of directives:
- public / private — public lets any cache, including shared CDNs, store the response. private restricts it to the end user's browser, which is essential for personalized or authenticated content.
- max-age=N — the response is fresh for N seconds in any cache before it must be revalidated. This is the directive you will use most.
- s-maxage=N — overrides max-age for shared caches only. It lets you cache aggressively on the CDN while keeping a shorter lifetime in the browser.
- no-cache — the response may be stored, but a cache must revalidate it with the origin before every reuse. Despite the name, it does not mean "don't cache."
- no-store — nothing is ever written to any cache. This is the only directive that truly disables caching, and it overrides everything else.
- must-revalidate — once a response is stale, a cache must check with the origin and must never serve the stale copy.
- immutable — promises the body will not change while it is fresh, so the browser skips revalidation even on reload. Ideal for fingerprinted files.
- stale-while-revalidate=N — for N seconds after going stale, a cache may serve the old copy instantly while fetching a fresh one in the background.
Recommended Cache-Control Values by Asset Type
There is no single "best" header — the right policy depends on whether the file's contents can change at its URL. Here are battle-tested defaults.
| Asset type | Recommended Cache-Control | Why |
|---|---|---|
| Fingerprinted JS/CSS (app.a1b2c3.js) | public, max-age=31536000, immutable | The filename changes on every deploy, so the file at a given URL never changes. Cache it for a year. |
| HTML pages | public, max-age=0, s-maxage=60, must-revalidate, stale-while-revalidate=86400 | Content changes, so the browser always revalidates, but the CDN holds a short copy and serves stale during refresh. |
| Images & media | public, max-age=86400, s-maxage=2592000, stale-while-revalidate=604800, no-transform | Long-lived on the CDN, refreshed daily in the browser, and protected from proxy recompression. |
| Authenticated / API responses | private, no-cache | Per-user data must never land in a shared cache and should always be revalidated for freshness. |
How to Set the Cache-Control Header
1. Decide if the content at the URL can change
This is the key question. If the URL is fingerprinted (the filename includes a content hash), the file is effectively immutable and you can cache it forever. If the same URL can return different content over time — like an HTML page — you need short or zero browser caching with revalidation.
2. Split the browser and CDN lifetimes
Use max-age for the browser and s-maxage for the CDN. A common pattern for dynamic HTML is a near-zero max-age with a longer s-maxage, so users always get a freshly revalidated page while your origin is shielded by the edge.
3. Add stale-while-revalidate for a smoother experience
Adding stale-while-revalidate lets caches serve the previous version instantly while quietly fetching a new one. Visitors never wait on a revalidation round-trip, and the cache stays warm.
4. Emit the header from the right place
You can set the header in your framework config, your origin server (Nginx add_header, Apache, or a .htaccess rule), or directly on your CDN. Setting it once at the edge keeps the policy consistent across every response.
Common Cache-Control Mistakes
- Caching HTML forever. If an HTML page has a long max-age, users keep seeing the old version until the cache expires — there is no way to push an update.
- Using no-cache to disable caching. no-cache still stores the response; only no-store prevents caching entirely.
- immutable without a max-age. immutable only applies while a response is fresh, so it does nothing without a positive max-age.
- private with s-maxage. s-maxage targets shared caches, which private already forbids — the directive is silently ignored.
- Forgetting fingerprints. Long-caching an unhashed asset means you cannot ship a fix without renaming the file or busting the cache manually.
Expert Tips
Fingerprint, then cache forever
Add a content hash to static asset filenames so each URL is immutable, then cache them for a year with the immutable directive. New deploys ship new URLs, so users never see stale code.
Keep HTML fresh, shield the origin
Give HTML a near-zero browser max-age with must-revalidate, but a short s-maxage and a stale-while-revalidate window on the CDN. Users always get current content while your origin handles only a trickle of traffic.
Frequently Asked Questions
What is the difference between max-age and s-maxage?
max-age sets the freshness lifetime for every cache, including the browser. s-maxage applies only to shared caches like CDNs and proxies, and when present it overrides max-age there. Use s-maxage to cache aggressively at the edge while keeping a shorter lifetime in the browser.
Does no-cache mean the response is not cached?
No. no-cache allows a cache to store the response but requires it to revalidate with the origin before every reuse. The only directive that prevents storage entirely is no-store, which you should reserve for sensitive or always-fresh data.
When should I use the immutable directive?
Use immutable for fingerprinted static assets whose URL changes on every deploy, paired with a long max-age such as one year. It tells the browser the body will never change while fresh, so it skips revalidation even when the user reloads the page — eliminating needless conditional requests.
What Cache-Control header should HTML pages use?
HTML usually changes, so avoid long browser caching. A robust pattern is a near-zero max-age with must-revalidate plus a short s-maxage on the CDN and a stale-while-revalidate window. Visitors always get a revalidated page, your origin is protected, and updates appear immediately.