← Back to blog
Tips & Tricks· October 5, 2026 ·7 min read

Cache-Control headers: the only guide you'll actually need

Cache-Control decides what gets cached, by whom, and for how long. It looks cryptic and is mostly six words you actually use. Here they are, plus the one distinction that trips everyone.

Mads Edelskjold
Mads Edelskjold
Founder, NordicCDN · ex-datacenter CTO
Cache-Control headers: the only guide you'll actually need
The short version

The Cache-Control header answers three questions: may this be cached, by whom, and for how long. Six directives cover almost every case — public, private, max-age, s-maxage, no-store and stale-while-revalidate. Cache fingerprinted assets for a year, give HTML a short freshness window with stale-while-revalidate, and keep genuinely personal responses out of shared caches.

Cache-Control has a reputation for being fiddly, mostly because the specification contains a lot of directives nobody uses. Strip it back to the ones that earn their place and it is straightforward. The complexity that remains is real, though, and it lives in one distinction that catches almost everyone — so we will get to that early.

The directives worth knowing

DirectiveWhat it saysWho obeys it
publicAny cache may store thisBrowsers and CDNs
privateOnly the user's own browser may store itBrowsers only
max-age=NFresh for N secondsEveryone
s-maxage=NFresh for N seconds in shared caches, overriding max-ageCDNs and proxies
no-storeDo not store this anywhere, at allEveryone
no-cacheStore it, but revalidate before every useEveryone
must-revalidateOnce stale, never serve without checkingEveryone
immutableThis will never change; do not revalidate on reloadBrowsers
stale-while-revalidate=NServe stale for up to N seconds while refreshing behind the scenesMainly CDNs
stale-if-error=NServe stale for up to N seconds if the origin errorsMainly CDNs

no-cache does not mean do not cache

This is the most misleadingly named directive in HTTP and it is worth being clear about. no-cache means "you may keep a copy, but you must check with the origin before using it". The response is still stored; it just cannot be served without a revalidation request.

no-store is the one that means do not cache. If you want a response never written to disk anywhere, that is the directive you want.

The practical difference matters: no-cache with an ETag is quite efficient, because the revalidation usually returns a 304 with no body. no-store means the full response is transferred every single time.

The one distinction that trips everyone: max-age vs s-maxage

Browsers and CDNs are both caches, but you rarely want them holding content for the same length of time.

Consider a news article. You want your CDN to cache it for an hour so your origin is protected. You do not want a visitor's browser holding it for an hour, because if you correct an error and purge the CDN, that visitor still has a stale copy and no way for you to reach it.

s-maxage solves this by applying only to shared caches:

Cache-Control: public, max-age=60, s-maxage=3600

The browser caches for a minute. The CDN caches for an hour. You can purge the CDN and be confident everyone gets the correction within sixty seconds, while your origin still only serves the page once an hour.

This is the shape you want for almost all HTML: short in the browser, long at the edge, purge on change. The reason it matters is that you can invalidate a CDN and you cannot invalidate a browser.

stale-while-revalidate, the underrated one

Normally when a cached response expires, the next visitor waits while the cache fetches a fresh copy. That visitor pays the full origin latency, which is a slightly unfair way to distribute the cost of freshness.

stale-while-revalidate changes the deal. Within the specified window past expiry, the cache serves the stale copy immediately and refreshes in the background. Nobody waits. The next visitor gets the fresh version.

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

Its sibling stale-if-error is worth adding wherever it is tolerable: if the origin returns a 5xx or times out, serve the stale copy rather than an error page. It is the cheapest resilience you will ever configure — your site staying up through an origin outage because the edge kept serving what it had.

Recipes

  • Fingerprinted static assets — app.a1b2c3.js, styles.d4e5f6.css

    public, max-age=31536000, immutable. A year, and never revalidated. Safe because the filename changes when the content does. immutable additionally stops browsers revalidating on a manual reload.

  • Unfingerprinted static files — /logo.png

    public, max-age=86400. A day, since you have no way to bust it other than waiting or renaming.

  • HTML you can purge

    public, max-age=60, s-maxage=86400, stale-while-revalidate=604800, stale-if-error=604800. Long at the edge, short in the browser, purge on publish.

  • HTML you cannot purge

    public, max-age=300, stale-while-revalidate=3600. Shorter, since expiry is your only invalidation mechanism.

  • API responses, public data

    public, max-age=0, s-maxage=60, stale-while-revalidate=300. Never trusted in the browser, briefly cached at the edge — which absorbs the majority of duplicate requests.

  • Personal pages — cart, account, dashboard

    private, no-store. Not no-cache. Nothing personal should sit in a shared cache under any circumstances.

  • A blanket no-store applied everywhere "to be safe" is the single most common cause of a bad cache hit ratio. It is also often applied by framework defaults without anyone choosing it. Check what your application actually sends before assuming your configuration is what is in effect.

    ETags and revalidation

    Cache-Control decides how long something is fresh. When it stops being fresh, validators decide whether it needs re-downloading.

    An ETag is a fingerprint of the response. On revalidation the client sends If-None-Match with that value, and if nothing changed the server replies 304 Not Modified with no body. Last-Modified works similarly with a timestamp and If-Modified-Since, at one-second precision.

    A 304 still costs a round trip, which is why it is not a substitute for a decent max-age — it saves bandwidth, not latency. Send validators anyway, because they make the expensive case much cheaper.

    Check what you are actually sending

    curl -sI https://example.com/ | grep -iE 'cache-control|etag|age|vary'

    Run it against your homepage, a static asset and an API endpoint. The Age header tells you how long the response has been sitting in a cache, which is a quick way to confirm the CDN is actually caching rather than just relaying.

    Frequently asked questions

    What does Cache-Control do?

    Cache-Control is an HTTP response header that instructs browsers and shared caches such as CDNs whether a response may be stored, who may store it, and for how long it remains fresh. It is the primary mechanism for controlling caching behaviour, taking precedence over the older Expires header.

    What is the difference between no-cache and no-store?

    no-cache permits a response to be stored but requires the cache to revalidate with the origin before serving it, so an unchanged response can be confirmed with a lightweight 304 reply. no-store forbids storing the response anywhere at all, meaning the full body is transferred on every request. Only no-store actually prevents caching.

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

    max-age applies to all caches, including the visitor's browser. s-maxage applies only to shared caches such as CDNs and proxies, and overrides max-age for them. The common pattern is a short max-age with a much longer s-maxage, so the CDN protects your origin while browsers hold content only briefly — important because you can purge a CDN but cannot purge a browser.

    What is stale-while-revalidate?

    stale-while-revalidate defines a window after a response expires during which a cache may serve the stale copy immediately while fetching a fresh one in the background. It removes the latency penalty that otherwise falls on whichever visitor happens to arrive first after expiry, so no individual user waits for the origin.

    How long should I cache static assets?

    One year — max-age=31536000 — with the immutable directive, provided the filenames contain a content hash such as app.a1b2c3.js. Because a change to the file produces a new filename, there is no staleness risk. Files without a fingerprint in the name should use something shorter, around a day, since renaming is the only way to invalidate them.

    Why is my content not being cached by the CDN?

    Usually because the response carries no-store, private or max-age=0, often added by a framework default rather than deliberately. Other frequent causes are a Set-Cookie header on responses that should be shared, or a Vary header on something with unlimited values such as User-Agent. Run curl -I against the URL and read the headers your origin actually sends.

    Set these once per content type and you will rarely think about the header again, which is exactly how a good caching configuration should feel.

    #caching #cache-control #http headers #performance
    Put it into practice

    See how NordicCDN does this for your site:

    Mads Edelskjold
    Written by
    Mads Edelskjold — Founder, NordicCDN · ex-datacenter CTO

    Mads has worked in IT — mostly hosting — since he was 16. He took an early stake in a SaaS company and helped grow it through to its acquisition by Visma, has built and run data-center networks, and served as CTO of a Danish data center. He started NordicCDN to make fast, secure infrastructure simple to use.

    Make your site load instantly

    Start free in two minutes — no card required.

    Start free