← All writing
Cloud & Infra·Sep 20, 2026·5 min read

The cache-control header that shipped last week's bug to everyone

The fix was deployed and correct within a minute. It just didn't reach anyone, because a `max-age` set months earlier had already decided how long the old version would keep being served.

H

Hammad Iqbal

Software Engineer

The bug fix deployed cleanly, the build passed, and the new bundle was live on the server within a minute. Users kept hitting the old bug for two more days. The static asset's `cache-control` header had been set to a generous `max-age` months earlier, back when long caching seemed like a straightforward performance win, and every edge node and browser that had already fetched the broken bundle had no reason to ask for it again until that window ran out. The deploy was correct. The header just didn't know a new version existed, because that was never its job.

In brief
  • A generous max-age is invisible until you need to take something back — then it's however long the header said, for whoever already cached it.
  • A CDN purge and a long cache lifetime are two different mechanisms, and most setups only have one of them working reliably.
  • Content-hashed asset filenames make the cache lifetime irrelevant instead of racing to purge it in time.

The header does exactly what you told it to

`cache-control: max-age=604800` isn't a bug when it's set — it's a deliberate trade, telling every browser and edge node that fetched the response that it's safe to keep serving that exact copy for a week without asking again. That's a good trade for a version of a file that will never change under that URL. It's a bad trade for a URL whose contents you plan to update, because the header has no way to know a new deploy happened; it only knows what it was told when the response was cached.

Purge is a request, not a guarantee

Invalidating a cached response after the fact means asking every location that might have a copy to drop it, and that's a weaker guarantee than it sounds like:

  • A CDN purge has to propagate to every edge node, and propagation isn't instant everywhere at once
  • `stale-while-revalidate` will keep serving the old response while it fetches the new one in the background, by design
  • Browsers that already cached the response locally aren't reachable by a server-side purge at all

Make the URL change instead of the cache

The pattern that avoids the race entirely is content-hashed filenames: `app.a1b2c3.js` instead of `app.js`, where the hash changes whenever the content does. The old file can keep its long `max-age` forever, because it's genuinely immutable — nobody will ever need to invalidate it, since a new deploy references a new filename instead of overwriting the old one. Only the entry point that points to the hashed filename needs a short cache lifetime, and that's a much smaller, much safer thing to get right.

Takeaway

A cache header isn't wrong just because it's long — it's wrong the moment you need to change something it's already promised not to re-check, and by then the fix is stuck behind the exact durability you asked for.

Have a project this kind of thinking applies to?

Tell me what you're building — I read every message myself.

Available for new projects

4+

Years exp.

8+

Projects shipped

<6h

Avg. reply time

Let's build something

Tell me about your project.

Share what you're building, your timeline, and the best way to reach you.