CloudFront Cache Invalidation Without Wasting Money
29 Dec 2025Every CloudFront invalidation past the free tier costs money, and even a free one takes minutes to propagate globally. Versioned filenames sidestep the entire problem: never invalidate, never wait, never pay.
Why invalidation is the wrong default tool
An invalidation tells every edge location to drop its cached copy of a path and re-fetch from origin. That’s expensive to run frequently, slow to complete (minutes, not seconds), and worse — during the propagation window, different users in different regions see different versions of the same file. You didn’t fix the staleness, you just moved it from “everyone sees old” to “some people see old, some see new, briefly.”
The first 1,000 paths invalidated per month are free; after that it’s a per-path charge, and a single invalidation request with a wildcard still counts against your path count in a way that adds up fast if it’s part of every deploy.
The pattern that avoids it entirely
Give every build a unique filename, and cache that filename forever:
/assets/app.a1b2c3.js
/assets/app.d4e5f6.js
Any modern bundler (Vite, webpack, esbuild) does this automatically via content hashing — the hash changes only when the file’s contents change. Set a long cache lifetime on these:
Cache-Control: public, max-age=31536000, immutable
A year, effectively forever, because the filename itself changes whenever the content does. There’s nothing to invalidate — the old file is still cached under its old name, but nothing references it anymore after deploy.
What still needs short caching
Your entry HTML file (index.html) references the hashed asset filenames, so it can’t be cached long — it’s the one file that has to be fresh on every deploy for the hashed references to update:
Cache-Control: no-cache
no-cache doesn’t mean “don’t cache” — it means “cache it, but revalidate with the origin before serving it,” via ETag. That’s cheap (a 304 response, no body transferred) and guarantees nobody serves a stale HTML file pointing at asset hashes that no longer exist.
The actual CloudFront config
Set cache behaviors per path pattern in your distribution:
| Path pattern | Cache-Control | TTL |
|---|---|---|
/assets/* (hashed) |
public, max-age=31536000, immutable |
Long |
/index.html, / |
no-cache |
Effectively 0, revalidated |
/api/* |
Usually no-store, bypass CDN cache |
0 |
When invalidation is still the right call
Not everything can be versioned this way. A CMS-driven page, a user-uploaded image at a fixed URL, an API response cached at the edge — these have stable URLs by requirement, so an invalidation (or a short TTL that expires naturally) is the actual tool for the job, not a workaround. Use it there, deliberately, and keep the blast radius small: invalidate specific paths, not /*, so you’re not paying to drop cache on content that hasn’t changed.
Gotcha
immutable in Cache-Control isn’t honored by every client the same way — some older browsers ignore it and still send a conditional revalidation request on reload, which CloudFront will answer with a 304. That’s harmless (cheap, no transfer), just don’t be surprised the network tab shows a request at all for a file you thought was cached forever. It is; the browser’s just checking politely.