HTTP Caching Cheat Sheet
Every Cache-Control directive, plus Expires, ETag, Vary — with correct examples and the pitfalls that bite most developers.
Cache-Control Directives
| Directive | What it does | Example | Note |
|---|---|---|---|
| max-age=<seconds> | Response stays fresh for <seconds> from the response date. Relative, no clock drift. | Cache-Control: max-age=3600 | Always preferred over Expires. |
| s-maxage=<seconds> | Shared cache (CDN) max-age. Overrides max-age for CDNs. Browsers ignore this. | Cache-Control: s-maxage=60, max-age=3600 | CDN caches 1 min; browser caches 1 hour. |
| public | Any cache (browser + CDN) may store the response. | Cache-Control: public, max-age=31536000, immutable | Default for static assets. |
| private | Only the user's browser cache. CDNs MUST NOT store. | Cache-Control: private, max-age=300 | User-specific responses. |
| no-cache | Cache may store, but MUST revalidate (conditional GET) before using. Does NOT mean don't cache. | Cache-Control: no-cache | Top HTTP mistake: confusing no-cache with no-store. |
| no-store | Don't store the response at all. Every request hits the origin. | Cache-Control: no-store | Bank balances, auth tokens, real-time data. |
| immutable | On page reload, skip revalidation — the resource hasn't changed. For hash-named static assets. | Cache-Control: public, max-age=31536000, immutable | Pairs with content-hash filenames. |
| must-revalidate | Before reusing a stale response, the cache MUST revalidate with the origin. | Cache-Control: max-age=3600, must-revalidate | Stricter than the default revalidation behaviour. |
| stale-while-revalidate | Serve stale content while asynchronously revalidating in the background. | Cache-Control: max-age=3600, stale-while-revalidate=600 | Great UX for APIs that change infrequently. |
| stale-if-error | Serve stale content if the origin server is unreachable. | Cache-Control: max-age=3600, stale-if-error=86400 | Graceful degradation for CDNs. |
Other Caching Headers
Expires: Thu, 01 Dec 2025 16:00:00 GMT
Absolute wall-clock expiry. Superseded by Cache-Control max-age. Clock drift makes it unreliable.
ETag: "33a64df5"
Opaque version identifier. Sent back as If-None-Match for conditional GETs. Server returns 304 Not Modified if unchanged.
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
Date-based version. Prefer ETag when both available.
Age: 24
How many seconds the response has been in a shared cache. Set by CDNs. Used to compute remaining freshness.
Vary: Accept-Encoding, Origin
Tells caches to partition the response by request header values. Critical for CORS and compression.
Pro tip: For static assets with content-hash filenames (main.abc123.js), use Cache-Control: public, max-age=31536000, immutable. Immutable tells the browser to skip revalidation on reload — zero requests to your server. See the full HTTP Caching Guide for in-depth explanations of each directive.