HAMZA KAREEM / CONCEPTS hamzawi.dev

Lesson 181 min read

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

  1. Can this be reused, and for how long? → Cache-Control
  2. 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?