What are the different layers of caching you can use on a website?
Multiple layers: HTTP cache (Cache-Control, ETag) for browser + intermediate caches; CDN (long max-age for hashed assets, short with revalidate for HTML); service worker for offline + custom strategies; in-memory app cache (React Query, Redux); localStorage/IndexedDB for persistent client data. Strategy: 'immutable + far-future' on versioned URLs (hashes), 'stale-while-revalidate' for content, 'no-store' for personalized/sensitive. Cache keys must include anything that changes the response (auth, locale, Vary headers).
Web caching is a stack — each layer catches what the layer above misses, and together they cut origin load by 90%+ for typical sites.
The layers (closest to user → furthest)
1. Browser HTTP cache
Built into every browser. Driven by response headers:
Cache-Control: public, max-age=31536000, immutable
ETag: "abc123"
Last-Modified: Wed, 17 May 2026 12:00:00 GMTmax-age— how long the response is fresh (no revalidation).immutable— promise that the resource won't change before max-age expires; browser skips even revalidation on user reload.ETag/Last-Modified— opaque/timestamp markers for conditional revalidation. Browser sendsIf-None-Match/If-Modified-Since; server returns 304 (empty body) if unchanged.s-maxage— like max-age but only for shared caches (CDNs).stale-while-revalidate=N— serve stale up to N seconds while fetching fresh in background.no-cache— cache but always revalidate.no-store— never cache (auth-only, personalized).private— only the browser, not shared caches.
Standard pattern:
- Hashed assets (
main.abc123.js,logo.def456.png):Cache-Control: public, max-age=31536000, immutable. Cached forever. - HTML:
Cache-Control: public, max-age=0, must-revalidateor short max-age + ETag. Always fresh on next nav.
2. CDN / edge cache
CloudFront, Cloudflare, Fastly, Vercel Edge. Shared across all users; cache HTML and assets close to user.
- Same headers (
Cache-Control: s-maxage=…). - Vary on
Accept-Encoding,Accept-Languageif responses differ. - Cache key normally URL; can extend with query whitelist, cookies.
- Purge by URL or tag (Cloudflare cache tags, Fastly surrogate keys).
3. Service Worker (custom strategies)
When the browser cache isn't expressive enough:
- Cache-first for static assets.
- Network-first with cache fallback for HTML (offline support).
- Stale-while-revalidate for content that's fine being slightly old.
- Network-only for auth, mutations.
- Pre-cache at install for the app shell.
Workbox makes this manageable.
4. In-memory app cache (React Query / RTKQ / SWR / Redux)
- Caches API responses by query key.
- Deduplicates concurrent requests.
- TTL and tag-based invalidation.
- Survives component unmount but not page reload.
This is where most "API caching" lives in modern apps.
5. localStorage / sessionStorage / IndexedDB
For client-side persistent state:
localStorage— 5–10MB, sync API, sticky across sessions.sessionStorage— same but per-tab.IndexedDB— gigabytes, async, structured queries. Right for big offline datasets, Dexie or idb wrapper for sanity.Cache API(within SW context) — Request/Response pairs, perfect for SW caching.
Watch out: never store PII or auth tokens in localStorage (XSS-readable). HttpOnly cookies for tokens.
Cache keys — the hard part
If two users get different responses for the same URL, the cache must distinguish:
- Auth:
Authorization: Bearer …responses are per-user;Cache-Control: private(browser only) orno-store. Don't put behind shared caches. - Locale:
Vary: Accept-Languageso the cache stores one per language. - Cookies: by default CDN caches strip cookies and ignore them; if your response varies by cookie, configure the cache key explicitly.
- Device: mobile vs desktop variants —
Vary: User-Agentis too broad (every UA string becomes a cache entry); use Client Hints or render mobile-first responsive.
Wrong cache key = users seeing each other's data. The most embarrassing cache bug.
Strategy matrix
| Resource | Cache layer | Header |
|---|---|---|
| Versioned JS/CSS | Browser + CDN forever | public, max-age=31536000, immutable |
| Images (versioned) | Browser + CDN forever | public, max-age=31536000, immutable |
| HTML (public) | CDN short + browser revalidate | public, max-age=0, s-maxage=60, stale-while-revalidate=300 |
| HTML (per-user) | No caching | private, no-store |
| API (public, slow-changing) | App cache + browser short | max-age=60, stale-while-revalidate=3600 |
| API (per-user) | App cache only (React Query) | private, no-store |
| Auth endpoints | Never | no-store |
Common pitfalls
- Long-cached HTML referencing a chunk that got rebuilt with a new hash → 404 on next deploy.
no-cacheconfused withno-store(no-cache does cache, just revalidates).- Mixing
Cache-ControlandExpires—Cache-Controlwins; removeExpiresto avoid confusion. - Forgetting
Varywhen serving compressed (gzip/brotli) responses (most CDNs handle this; double-check). - Setting long cache on
/api/me→ user sees previous user's data. - Service worker caching old shell + serving new chunks → fatal version mismatch.
Invalidation
The two hard problems in computer science: naming things, cache invalidation, and off-by-one errors.
- Versioned URLs (hashes): never invalidate; serve forever, change the URL on update. Use for assets.
- Soft purge / revalidate: CDN tag-purge an entire content type (e.g., all product pages after a price change).
- TTL: accept some staleness; pick TTL to match how often the content changes.
- stale-while-revalidate: serve stale instantly, fetch fresh in background — best of both for non-personal content.
Mental model
Cache aggressively at the layer closest to the user. Use immutable URLs to make cache invalidation a non-problem. Personalize at the closest layer to the user too (browser, edge with auth-aware Workers) so the rest of the stack can cache shared content.
Follow-up questions
- •What's the difference between no-cache and no-store?
- •How do you cache personalized HTML at the edge?
- •What is stale-while-revalidate and when does it apply?
- •How do you handle cache invalidation across thousands of CDN nodes?
Common mistakes
- •Caching personalized content shared at the CDN — data leak between users.
- •no-cache and no-store mix-up — no-cache still caches.
- •Forgetting Vary on responses that legitimately differ.
- •Long-cached HTML + frequently updated assets without graceful version handling.
- •Storing tokens in localStorage — XSS leaks them.
- •Service worker caching old app shell + new chunks → broken app.
Performance considerations
- •Done right, cache hits return in <50ms (browser), <100ms (CDN). A 90% cache hit rate cuts origin load 10x and lets the origin be small/cheap. Cache misses are unavoidable but should be rare — measure hit ratio per resource type.
Edge cases
- •Range requests for video/audio interact with cache differently — most CDNs handle.
- •HTTP/2 push (deprecated) was an alternative to preload + cache; Early Hints (103) is the modern equivalent.
- •Cookie-keyed caching scales poorly (high cardinality) — extract relevant signal into a header instead.
- •Brotli vs gzip — same URL, different encoding — Vary: Accept-Encoding handles it.
- •Cache freshness on back/forward navigation differs from forward navigation (bfcache).
Real-world examples
- •Major sites use Cloudflare/Fastly + tag-based invalidation for HTML.
- •Next.js + Vercel: ISR + edge cache + tag invalidation built-in.
- •Hashed-filename builds (webpack/vite) are the standard for assets.
- •GitHub's API uses ETag heavily — cached requests don't count against rate limit.