HTTP Cache-Control Headers What no-cache, no-store, and max-age Actually Mean
A practical breakdown of Cache-Control directives — what browsers and CDNs actually do with no-cache, no-store, max-age, s-maxage, and ETags. Including the mistakes that bite most developers in production.
You’ve probably written Cache-Control: no-cache and assumed the browser would skip the cache entirely. It won’t. no-cache means “revalidate before serving from cache.” If you actually want the response to never be stored, that’s no-store. This confusion causes stale data bugs in production that are annoying to diagnose because everything looks fine in the Network tab on first load.
Let’s go through each directive clearly — what browsers do with it, what CDNs do with it, and the mistakes that come from mixing them up.
The Full Cache-Control Directive Reference
| Directiva | Browser behavior | CDN / proxy behavior | Typical use |
|---|---|---|---|
max-age=N | Cache for N seconds | Cache for N seconds (unless s-maxage overrides) | Static assets, API responses |
s-maxage=N | Ignored | Cache for N seconds | CDN TTL separate from browser |
no-cache | Cache but revalidate every request | Cache but revalidate every request | Frequently-changing content with ETags |
no-store | Do not store anywhere | Do not store anywhere | Auth responses, sensitive user data |
must-revalidate | No stale serving — revalidate or fail | No stale serving — revalidate or fail | API responses where stale = broken |
proxy-revalidate | Ignored | No stale serving on shared caches | CDN-specific must-revalidate |
private | Browser may cache | Must not cache | User-specific pages |
public | Any cache may store | May cache | Shared static resources |
immutable | Never revalidate within max-age | Varies by CDN | Hashed / versioned assets |
stale-while-revalidate=N | Serve stale for N seconds while fetching fresh | Varies by CDN | Speed without hard staleness |
max-age and s-maxage: Browser vs CDN TTL
max-age=N tells both the browser and CDNs how many seconds the response is fresh. After N seconds, the cached response is stale and must be revalidated before use.
s-maxage=N is CDN-only. Browsers ignore it entirely. If you want a CDN to cache for an hour but the browser for only 5 minutes:
Cache-Control: max-age=300, s-maxage=3600
The browser caches for 5 minutes. CloudFront, Fastly, Nginx — they’ll use 3600 seconds. A common trap: setting max-age=0 thinking it disables caching. It doesn’t. max-age=0 means the response is immediately stale — the browser still caches it and revalidates, likely getting a 304. If you never want it cached, use no-store.
no-cache: Not What You Think
Cache-Control: no-cache does not mean “don’t use the cache.” It means “don’t serve from cache without revalidating with the server first.”
The sequence when a browser has a cached response with no-cache:
- New request arrives for the cached resource
- Browser sends the cached response’s ETag (via
If-None-Match) or Last-Modified timestamp to the server - If content unchanged → server returns
304 Not Modifiedwith no body - If content changed → server returns
200 OKwith the new response
The benefit over no-store: you still get the bandwidth savings of 304 responses. If your content changes occasionally but must be fresh when it does, no-cache combined with an ETag is the right combination.
no-store: The Real “Don’t Cache This”
Cache-Control: no-store means the response must not be stored anywhere — not in the browser cache, not in a CDN, not in an intermediate proxy. No copy, full stop.
Use this for:
- Authentication responses (login tokens, session data)
- Sensitive personal data
- One-time content (payment confirmations, OTP pages)
One subtlety: no-store doesn’t prevent a page from appearing in the browser’s back-forward cache (bfcache). Browsers keep an in-memory snapshot for navigation performance that’s separate from the HTTP cache. If you need to handle post-logout back-button issues, hook into the pageshow event and check event.persisted.
must-revalidate: No Stale Grace
HTTP caching specs allow caches to serve stale responses when the origin is unreachable — a resilience feature most developers don’t know about. must-revalidate removes that leeway: once a cached response is stale, the cache must revalidate or return a 504. No stale serving under any circumstances.
# Without must-revalidate: CDN may serve stale if origin is slow or down
Cache-Control: max-age=3600
# With must-revalidate: stale = error, not a fallback
Cache-Control: max-age=3600, must-revalidate
Use this for API responses where serving stale data breaks functionality — inventory counts, pricing, auth state — rather than just looks slightly wrong.
private vs public: The CDN Bug That Leaks User Data
private means the response is intended for a specific user. Browsers can cache it; shared caches (CDNs, reverse proxies) must not.
public explicitly allows any cache — including shared — to store the response. Some caches only cache authenticated-request responses if you explicitly mark them public.
The real-world bug this causes: a developer copy-pastes Cache-Control: public, max-age=3600 from a static asset onto a page that includes user-specific data. The CDN caches the response. User B makes the same request and gets User A’s page from cache. This isn’t theoretical — GitHub had a variant of this in 2018. Mark authenticated or user-specific responses private explicitly, even if you think your CDN “knows” not to cache them.
ETags and Conditional Requests
ETags are how the server says “here’s a fingerprint of this response.” The browser stores the ETag alongside the cached response and sends it back on the next request via If-None-Match. If content hasn’t changed, the server returns 304 Not Modified with no body — same freshness enforcement as no-cache, much less bandwidth.
El no-cache + ETag flow:
→ GET /api/config HTTP/1.1
← HTTP/1.1 200 OK
Cache-Control: no-cache
ETag: "abc123"
[full response body]
→ GET /api/config HTTP/1.1
If-None-Match: "abc123"
← HTTP/1.1 304 Not Modified
[no body — browser uses its cached copy]
Two ETag types:
- Strong ETags (
"abc123") — byte-for-byte identical. Required for CDN range request support. - Weak ETags (
W/"abc123") — semantically equivalent but not necessarily byte-identical. Fine for browser revalidation, not for range requests.
Nginx generates ETags automatically from file mtime and size. Express doesn’t add them by default — use app.set('etag', 'strong') o el etag middleware explicitly.
Last-Modified and If-Modified-Since
Same concept as ETags but coarser — timestamp-based rather than content-hash-based. The server includes Last-Modified; the browser sends If-Modified-Since on subsequent requests.
The problem: if you redeploy and file modification times update even though content didn’t change, caches invalidate unnecessarily. A content-hash-based ETag won’t have this problem. Use ETags where possible and treat Last-Modified as a fallback for servers that don’t support ETags.
Vary: The Header That Silently Multiplies Your Cache
El Vary header tells caches that the response may differ based on other request headers. Each unique combination of those headers gets its own cache entry.
Vary: Accept-Encoding
This tells caches to store separate responses for gzip, brotli, and identity encoding. Correct and common. The dangerous one: Vary: Cookie. Every user has a unique cookie set, so every user gets their own cache entry — effectively disabling shared caching. Many frameworks add Vary: Cookie silently. If your CDN cache hit rate is inexplicably low despite generous max-age values, check your response headers for Vary: Cookie sneaking in from session middleware.
Vary: * means “don’t cache this at all” in practice — every request is treated as unique. It’s equivalent to no-store for CDNs.
Cache-Busting with Query Parameters
When you need to force fresh downloads of versioned assets, appending a query param is the standard approach — the query string is part of the URL, so it’s treated as a new resource by both browsers and CDNs:
/app.js?v=2.1.4
/styles.css?hash=a1b2c3d4e5f6
If you’re constructing cache-busting params dynamically from content hashes or version strings that might contain special characters, make sure to percent-encode them before appending. The Codificador/Decodificador de URL handles that quickly if you’re testing or building URLs manually.
The Three Mistakes That Bite Most Developers
1. Using no-cache when you mean no-store. If you’re handling auth responses, logout endpoints, or anything with PII, you want no-store. no-cache leaves data in the browser cache (just flagged as stale); no-store removes the footprint entirely. The difference matters when users share devices.
2. Not setting s-maxage for CDN control. Without s-maxage, your CDN uses max-age. If max-age is short for browser freshness (say, 60 seconds), your CDN caches for 60 seconds too — which probably isn’t what you want. Separate the two TTLs explicitly.
3. public on endpoints that return user data. This one is the security incident, not just a performance bug. Any response that’s personalized or authenticated should be private. Default to private and opt into public only for genuinely shared resources.
Putting It Together
The mental model: no-cache is about freshness enforcement — the response lives in the cache, it just needs a server stamp of approval before use. no-store is about leaving no trace. max-age is your browser TTL. s-maxage is your CDN’s separate TTL. ETags are what make revalidation cheap.
Get the private/public distinction right on any endpoint that touches user data. That single mistake — copying a cache header from a static asset onto an authenticated endpoint — is the one that turns into a cross-user data leak when your CDN starts caching personalized responses.
Instalar extensiones
Agregue herramientas IO a su navegador favorito para obtener acceso instantáneo y búsquedas más rápidas
恵 ¡El marcador ha llegado!
Marcador es una forma divertida de llevar un registro de tus juegos, todos los datos se almacenan en tu navegador. ¡Próximamente habrá más funciones!
Herramientas clave
Ver todo Los recién llegados
Ver todoActualizar: Nuestro última herramienta was added on May 29, 2026
