CDN Cache Invalidation: Strategies That Don't Break in Production
"There are two hard problems in computer science." We've worked on the cache-invalidation one for a while. The patterns that hold up at scale and the ones that look clean and aren't.
Key takeaways
- Avoid purging wherever you can.
- Content-hash static asset filenames and cache them for a year as immutable, give HTML and APIs short s-maxage values with stale-while-revalidate, and reserve explicit purges for urgent edits.
- Use cache tags, now on CloudFront, Cloudflare and Fastly, when one change touches many URLs.
On this page
The invalidation strategy that holds up is to need as few purges as possible: content-hash static filenames and cache them for a year with immutable, give HTML and API responses a short s-maxage plus stale-while-revalidate, and purge only for urgent edits. When one change touches many URLs, purge by cache tag instead of by path.
Verified against
Amazon CloudFront Developer Guide (path and tag invalidation, invalidation billing), Cloudflare purge-by-tag docs, and Fastly surrogate key docs, against RFC 9111 and RFC 5861. Checked October 2026.
The "send a purge" approach works until you hit its edges: purges take time to propagate, every provider rate-limits or bills them, and a forgotten URL keeps serving stale content.
The shapes of the problem
Content falls into four shapes: hashed static assets (/app.a1b2c3.js, where the hash is the version), cacheable API responses, dynamic but cacheable HTML such as post pages, and user-specific content that usually shouldn't touch the CDN at all. Treating them all the same is where teams get into trouble.
Strategy 1: Versioned URLs (the easy case)
Every static asset has a content hash in its filename. app.css becomes app.a1b2c3.css. When you change the source, the hash changes, the URL changes, and the CDN treats it as a new resource.
Cache-Control on these:
Cache-Control: public, max-age=31536000, immutable
One year, and immutable tells browsers not to revalidate while the copy is fresh. The hash gives you correctness; the long TTL gives you performance.
This is the gold standard, and most build tools (Webpack, Vite, esbuild) produce hashed names automatically. The HTML that references those assets needs a short TTL, because it changes on every deploy.
Strategy 2: Cache-Control with sensible TTLs
For content that can't be content-hashed (blog post pages, dynamic API endpoints):
Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400
What this means:
s-maxage=3600: shared caches (the CDN) keep it fresh for 1 hour.stale-while-revalidate=86400: for up to 24 hours after it goes stale, the cache may serve the stale copy while it fetches a fresh one in the background (RFC 5861).
This is the underrated piece: users always get a fast cached response while freshness updates happen behind the scenes. Cloudflare reports these responses as cf-cache-status: UPDATING.
Tune the TTL per content type:
- High-update content (homepage with latest posts): 5 min
s-maxage - Stable content (FAQ, terms page): 1 day
s-maxage - Truly static within a deploy (compiled output): a year, hashed
Strategy 3: Explicit invalidation when needed
For content that needs to change immediately (a published post with a typo, a withdrawn product), use explicit invalidation. On CloudFront there are two mechanisms.
Invalidation paths. Submit the exact paths, or a wildcard:
aws cloudfront create-invalidation \
--distribution-id E2EXAMPLE123 \
--paths "/blog/cdn-cache-invalidation-strategies" "/images/*"
aws cloudfront get-invalidation --distribution-id E2EXAMPLE123 --id I3EXAMPLE456
Billing is per path, not per file: the first 1,000 paths a month are free across the whole AWS account, and a wildcard like /* counts as one path no matter how many objects it clears. Beyond that you pay per path (see the CloudFront pricing page for the current rate). Use paths for one-offs.
Versioned origin paths. Instead of invalidating, change the URL. The post becomes /blog/cdn-cache-invalidation-strategies?v=2 (if query strings are in the cache key) or gets a content hash in the path. The CDN treats it as a new resource. Same caching, no invalidation needed, but every page linking to the old URL has to change too.
Strategy 4: Surrogate keys and cache tags
Tag responses with logical IDs, then invalidate by tag. Every product page gets a header like Surrogate-Key: product-123; when product 123 changes, you purge that one key and every cached response carrying it is invalidated, across however many URLs.
All three major CDNs support this as of October 2026:
| CDN | Tag header | Purge call | Plan / setup | Notable limits |
|---|---|---|---|---|
| Fastly | Surrogate-Key (space-separated) | Purge by key API | Standard caching | 1 KB per key, 16 KB header |
| Cloudflare | Cache-Tag (comma-separated) | Purge by tag API | All plans, including Free | 16 KB header; Free plan 5 tag purges/min |
| Amazon CloudFront | Any header you name in CacheTagConfig | create-invalidation --paths "#tag" | Opt-in per distribution | 50 tags per object, 256 chars per tag |
CloudFront's version is the newest of the three and changes an old workaround. Teams used to keep a mapping table of "content ID to cached URLs" and invalidate each path; now the origin returns a header such as x-amz-meta-cache-tag: product:123, category:tv and you invalidate #product:123. Each tag counts as one path against the same 1,000 free paths. One gotcha from AWS's own docs: if you rename the tag header in CacheTagConfig, objects cached under the old header name no longer match tag invalidations, so send both headers during the switch.
A worked example: a Next.js blog behind a CDN
For a typical content site we would use the TTLs in the cheat sheet below, tag every post page post:<id>, and purge that tag on each admin edit. Without the edit-time purge, readers could wait up to an hour to see a fix. One trap here: framework-level revalidation such as Next.js revalidatePath refreshes the framework's own cache, not a third-party CDN sitting in front of it. If you add CloudFront or Fastly on top of your host, you need the CDN purge call as well.
What fails in production
Short TTLs everywhere. "Just cache for 60 seconds so it's always fresh-ish." Sounds safe. Result: popular pages hit origin every minute per edge location, which negates most of the caching benefit. The cache only helps if the TTL is meaningfully longer than the gap between requests for that content.
Wildcard invalidation on every deploy. --paths "/*" after each deploy is one billable path, so cost isn't the issue; the issue is that it throws away every cached object, sends a thundering herd at your origin, and hides the fact that your assets aren't hashed. Hash your assets so deploys don't need invalidation.
Caching logged-in content. Short per-user TTLs risk serving one user's page to another if the key is wrong, and varying on the auth cookie creates one entry per session and drives the hit rate toward zero. Use Cache-Control: private and keep the CDN out of it.
Watch the hit ratio, not the purge log
Track cache hit ratio per path pattern and origin request rate. A hit-ratio drop or an origin spike after a deploy means cache busting you didn't expect, and a climbing daily invalidation count means purges are covering for unhashed assets. The general theory is in what is a CDN; the application-side equivalents are in our caching patterns guide.
Per-content-type guidance
A cheat sheet:
| Content type | Strategy |
|---|---|
| Compiled JS/CSS with hash | max-age=31536000, immutable |
| Logo, favicons | max-age=86400 (changes rarely; deploy bumps if needed) |
| Blog post HTML | s-maxage=3600, stale-while-revalidate=86400 + tag purge on edit |
| Sitemap, RSS | s-maxage=21600 |
| OG images / social-share images | s-maxage=604800 |
| User-specific (logged-in pages) | Cache-Control: private; don't cache at CDN |
| API: stable list endpoints | s-maxage=300 |
| API: user-specific | Cache-Control: private |
| API: real-time data | Cache-Control: no-cache |
Questions people ask
How long does a CloudFront invalidation take?
It is not instant, and the CLI returns as soon as the request is accepted, not when edges have dropped the objects. Run aws cloudfront get-invalidation and wait for Completed before declaring the fix live. Because of that lag, invalidation is a poor fit for routine deploys: content-hash your assets so new versions get new URLs, and keep invalidation for urgent edits to HTML or API responses that can't be versioned.
What is the difference between purging and cache-busting?
Purging tells the CDN to drop objects it already holds, by URL, wildcard or tag. Cache-busting changes the URL itself, usually with a content hash or version parameter, so the CDN sees a brand-new object and the old one simply ages out. Busting is instant and free; purging is needed when the URL can't change, such as a page people have bookmarked.
Does Cloudflare purge by tag work on the free plan?
Yes, as of October 2026. Cloudflare's docs list purge by tag, hostname and prefix on all plans, including Free. Limits scale by plan: Free allows 5 of these purge requests per minute with up to 100 operations each, while Enterprise allows 50 per second. Tag your responses with the Cache-Tag header, comma-separated, kept under 16 KB in total.
The call we'd make
Content-hash everything you can, put stale-while-revalidate on everything you can't, and classify content into a handful of TTL tiers instead of picking one number. Use explicit purges only for user-visible edits, and make them tag-based now that every major CDN supports it. Never cache logged-in content at the CDN. Then watch hit ratio per path, because it catches misconfiguration faster than anything else. Cache invalidation isn't hard per content type; it's hard when you have many content types and one strategy.
Sources
Cite this article
Kiril Urbonas (2026). CDN Cache Invalidation: Strategies That Don't Break in Production. DevOpsNess. Updated 2026-10-31. https://www.devopsness.com/blog/cdn-cache-invalidation-strategies
[CDN Cache Invalidation: Strategies That Don't Break in Production](https://www.devopsness.com/blog/cdn-cache-invalidation-strategies) (Kiril Urbonas, DevOpsNess, updated 2026-10-31)
More from Cloud
Explore more articles in this category
.NET 8 Support Ends November 10: The Lambda and Azure Functions Checklist
.NET 8, .NET 9, the Lambda dotnet8 runtime and the Azure Functions in-process model all lose support on November 10, 2026. Inventory commands, the isolated-worker migration and a CI gate.
Google Cloud's europe-west2 Outage: One Zone Lost Power and the Region Stalled for 6 Hours
On October 20, 2026, europe-west2-b lost power when its generators never started. Cross-zone control-plane timeouts then degraded the whole London region. What happened and the gcloud audit to run now.
Lambda Python 3.10 Is Deprecated Today: Find Every Function and Move to 3.13
AWS deprecated the Lambda python3.10 runtime on October 31, 2026. Functions keep running, patches stop. The inventory commands, IaC diffs and AL2023 gotchas for moving to python3.13.
You might have missed
Evergreen posts worth revisiting.