Anúncios incomodam? Ir Sem anúncios Hoje

HTTP Cache-Control Headers What no-cache, no-store, and max-age Actually Mean

Atualizado em

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.

HTTP Cache-Control Headers: What no-cache, no-store, and max-age Actually Mean 1
ANUNCIADO Remover?

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

DiretivaBrowser behaviorCDN / proxy behaviorTypical use
max-age=NCache for N secondsCache for N seconds (unless s-maxage overrides)Static assets, API responses
s-maxage=NIgnoredCache for N secondsCDN TTL separate from browser
no-cacheCache but revalidate every requestCache but revalidate every requestFrequently-changing content with ETags
no-storeDo not store anywhereDo not store anywhereAuth responses, sensitive user data
must-revalidateNo stale serving — revalidate or failNo stale serving — revalidate or failAPI responses where stale = broken
proxy-revalidateIgnoredNo stale serving on shared cachesCDN-specific must-revalidate
privateBrowser may cacheMust not cacheUser-specific pages
publicAny cache may storeMay cacheShared static resources
immutableNever revalidate within max-ageVaries by CDNHashed / versioned assets
stale-while-revalidate=NServe stale for N seconds while fetching freshVaries by CDNSpeed 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:

  1. New request arrives for the cached resource
  2. Browser sends the cached response’s ETag (via If-None-Match) or Last-Modified timestamp to the server
  3. If content unchanged → server returns 304 Not Modified with no body
  4. If content changed → server returns 200 OK with 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.

O 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') ou o 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

O 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.

Montando Tudo

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.

Quer eliminar anúncios? Fique sem anúncios hoje mesmo

Instale nossas extensões

Adicione ferramentas de IO ao seu navegador favorito para acesso instantâneo e pesquisa mais rápida

Ao Extensão do Chrome Ao Extensão de Borda Ao Extensão Firefox Ao Extensão Opera

O placar chegou!

Placar é uma forma divertida de acompanhar seus jogos, todos os dados são armazenados em seu navegador. Mais recursos serão lançados em breve!

ANUNCIADO Remover?
ANUNCIADO Remover?
ANUNCIADO Remover?

Notícias com destaques técnicos

Envolver-se

Ajude-nos a continuar fornecendo ferramentas gratuitas valiosas

Compre-me um café
ANUNCIADO Remover?