HTTP Caching (Cache-Control & ETags)
the deploy your users couldn't see
Web developmentweb development / web performance
The scenario
You ship a CSS fix at 9am. QA confirms it live. By noon, support is full of "still broken for me." You hard-refresh — it's fine. Their browser is happily serving styles.css from three hours ago. Meanwhile your /api/products endpoint gets hammered with identical requests you could have cached but didn't. Same feature, HTTP caching, pulling in opposite directions.
Every response answers two questions
- Can this be reused, and for how long? → Cache-Control
- Is my cached copy still fresh? → ETag / Last-Modified (validation)
Cache-Control, the directives that matter
Cache-Control: max-age=3600 # reuse for 1h, no server contact
Cache-Control: no-cache # DO cache, but revalidate every time
Cache-Control: no-store # never cache (private/sensitive data)
Cache-Control: private # browser only, not a shared CDN/proxy
Gotcha: no-cache ≠ no-store. no-cache still stores the copy, it just checks freshness before reusing. This trips people up constantly.
Validation with ETag (saves bandwidth)
Server tags the response with a fingerprint:
ETag: "v3-abc123"
Next request, the browser asks:
If-None-Match: "v3-abc123"
If nothing changed, server replies 304 Not Modified — empty body, no re-download. Fast and cheap.
Fixing the scenario
- HTML entry point → Cache-Control: no-cache (always revalidate; it's the door to everything else)
- Hashed assets like styles.a1b2c3.css → Cache-Control: max-age=31536000, immutable (cache for a year — a new build = a new filename). THIS is the pattern that kills stale-asset bugs.
- API GET → max-age=60, stale-while-revalidate=300 (serve instantly, refresh in the background)
The mistake that caused it
Serving styles.css (no hash in the name) with a long max-age. The URL never changes, so the browser has no reason to re-fetch — your fix stays invisible until the cache expires. Fix: content-hash the filename OR keep max-age short. Pick one; don't do neither.
Your exercise
Open DevTools → Network on a site you run. Pick one static asset. What's its Cache-Control, and is the filename content-hashed? If it's cached long but the name is NOT hashed, you're sitting on a latent stale-deploy bug. What one header or build change would you make to fix it?