"Caching isn't something you add on top — it's a protocol the server speaks fluently or accidentally breaks. Get the headers right and CDNs, browsers, and reverse proxies all cooperate. Get them wrong and you ship stale data to one user and miss-cache another user's auth response."
The Five Headers That Run Caching
Cache-Control— the modern, comprehensive directive. Everything else is a fallback or refinement.ETag— opaque version tag for conditional revalidation (Track 2 Lesson 4).Last-Modified— date-based validator, used withIf-Modified-Since.Expires— absolute expiration date. Pre-HTTP/1.1; overridden by Cache-Control when both present.Vary— tells caches which request headers to add to the cache key.
Cache-Control Directives You Actually Use
Cache-Control values are comma-separated directives. The combinations matter:
max-age=N— fresh for N seconds. Caches may serve without re-checking.s-maxage=N— like max-age but for SHARED caches only (CDN, proxy). Overrides max-age for them. Lets you set short browser TTL + long CDN TTL.public— any cache may store this response. Required for CDN caching of authenticated responses.private— only the requesting browser (private cache) may store. CDNs and proxies must not cache.no-cache— store it, but ALWAYS revalidate before serving (send conditional GET). Misleadingly named; it doesn't mean "don't cache."no-store— actually don't cache. Period. For sensitive data, payment forms, anything that must not persist.must-revalidate— once expired, MUST revalidate; don't serve stale even if the network is down.stale-while-revalidate=N— serve stale for N seconds while revalidating in the background. Modern, browser-supported, great UX win.immutable— this response will never change in its lifetime. Skip revalidation entirely. Use on fingerprinted asset URLs (app.abc123.css).
The Three Patterns You Reach For Most
1. Versioned static assets (CSS, JS, images with hash in filename):
Cache-Control: public, max-age=31536000, immutable
A year, never revalidate. Safe because the filename changes when the content changes — clients fetch the new URL.
2. Frequently-changing API responses (lists, user dashboards):
Cache-Control: private, max-age=60, must-revalidate
ETag: "v17-abc"
Vary: Accept-Encoding, Authorization
Brief browser cache, conditional GET after 60s, never cached by CDN. The ETag enables 304 responses; the Vary keeps per-user data separated.
3. Personal / sensitive data (auth responses, payment forms):
Cache-Control: no-store
Never cached anywhere. The only safe choice for tokens, session data, anything that must not persist on disk.
The default is silently wrong. An API endpoint with no Cache-Control header gets cached by intermediaries based on heuristics — sometimes for minutes, sometimes for hours. ALWAYS set Cache-Control explicitly, even if it's just
no-store or private, max-age=0. Implicit caching is where production data leaks live.cwkPippa's Cache Headers
cwkPippa's API endpoints default to
Cache-Control: no-store — its data is per-user and the WebUI is the only client. Static assets served by Vite get Cache-Control: public, max-age=31536000, immutable on hashed bundles (default Vite output) and no-cache on index.html so deploys take effect immediately. cwk-site's blog posts get public, s-maxage=300, stale-while-revalidate=86400 — short CDN freshness for editorial control, long stale-allowed for resilience. The pattern matches the data's freshness needs.