How CDN cache invalidation actually works
How CDN cache invalidation uses TTL expiry, purge APIs, surrogate keys, stale-while-revalidate, and versioned URLs to keep edge caches consistent with origin servers.
Why CDN invalidation is hard
A CDN keeps copies of origin content close to users. That improves latency and reduces origin load, but it also means a publish or price change creates multiple cached versions of the same object. A browser, edge cache, shield, and intermediate proxy may each have a different expiry or copy.
The core design question is not simply βhow do I purge?β It is how to choose freshness, cost, and availability behavior for each content type, then handle the interval during which invalidation is still propagating.
Scope and assumptions
This article covers a multi-tier CDN with an origin, shield or regional cache, and many edge points of presence. It covers a single URL, a related set of objects, and static assets. Provider propagation times and purge APIs differ, so the examples are representative rather than guarantees for a particular vendor.
30-second mental model
Treat the origin as the source of truth and caches as eventually consistent replicas:
- Set a passive safety net: TTL and revalidation limit how long an update can remain stale.
- Choose an active mechanism: purge by URL or tag when waiting for TTL is unacceptable.
- Avoid invalidation where possible: versioned asset URLs make new content addressable without deleting old copies.
- Control the propagation gap: use
stale-while-revalidate, request coalescing, cache warming, and explicit freshness messaging. - Observe every layer: inspect cache status,
Age, validators, purge completion, and origin load.
The design has five layers:
- TTL-based expiry: The passive, time-based approach that every CDN uses as the baseline
- Active purge and ban APIs: How you force immediate invalidation when TTL is too slow
- Surrogate keys and tag-based invalidation: Surgical invalidation of all content related to a specific entity
- Versioned URLs and content hashing: Sidestepping invalidation entirely by changing the URL
- Multi-layer cache propagation: How invalidation flows from origin through shield to 300+ edge locations
The Architecture
Here is how a multi-tier CDN handles content serving and invalidation. The key insight is that invalidation is not one operation. It is a cascade that must traverse every layer of the cache hierarchy.
Five-minute end-to-end flow
Here is the serving and invalidation flow step by step.
When a user requests content, their DNS resolves to the nearest edge PoP (Point of Presence). The edge checks its local cache. On a cache hit, the response goes back in 5-20ms. On a miss, the edge forwards the request to a shield node. The shield is a second cache layer that aggregates requests from multiple edge nodes in the same region. This reduces origin load by ~90%.
If the shield also misses, it fetches from the origin server. The response flows back through the shield (which caches it) to the edge (which also caches it) to the user.
When content changes, the origin needs to invalidate every copy across this entire chain. That is where the four invalidation strategies come in.
TTL: The Passive Baseline
TTL (Time To Live) is the simplest invalidation strategy: set an expiry time on cached content and let it expire naturally. When the TTL for a cached object passes, the next request for that object triggers a revalidation with the origin.
The origin controls TTL via HTTP headers:
Cache-Control: public, max-age=3600, s-maxage=86400
max-age=3600 tells the browser cache to hold the content for 1 hour. s-maxage=86400 tells the CDN (shared cache) to hold it for 24 hours. The s-maxage directive overrides max-age for shared caches, letting you set different TTLs for browser and CDN.
The tradeoff is straightforward: short TTLs mean fresher content but more origin traffic. Long TTLs mean better performance but stale content after updates.
Notice the 304 Not Modified response. When the TTL expires and the edge revalidates, the origin can respond with 304 if the content has not actually changed. This saves bandwidth because the full response body is not retransmitted. The edge uses the ETag or Last-Modified header from the original response to make this conditional request.
The problem with TTL-only: if you fix that typo in the headline, you have to wait up to s-maxage seconds before all edge caches expire and fetch the updated content. With a 24-hour TTL, some users will see the typo for up to a full day.
Active Purge: When TTL Is Too Slow
Active purge is the "pull the fire alarm" button. Instead of waiting for caches to expire, you tell the CDN to delete specific cached objects immediately. Every major CDN provider offers a purge API, but they work differently.
Fastly: Its purge API can mark objects stale quickly, with provider-specific propagation behavior. The next request then fetches fresh content from the origin. Treat any advertised purge time as a measured provider SLO, not as an atomic global guarantee.
Cloudflare: Purge completes in under 30 seconds for most PoPs. Supports purge by URL, purge by tag (via Cache-Tag header), and purge everything. Their "purge everything" is fast but aggressive. It invalidates the entire zone, not just one URL.
CloudFront: Invalidation requests are queued and processed asynchronously. Propagation takes 5-15 minutes to complete across all edge locations. There is no "instant purge" option. CloudFront gives you 1,000 free invalidation paths per month; additional ones cost $0.005 each.
A common design mistake is saying "just purge the cache" without acknowledging propagation delay. Depending on the provider and request, some edges may continue serving stale content during the gap. You need an explicit strategy for that interval.
Here is how the CDN provider comparison looks in practice:
| Provider | Purge speed | Purge by tag | Wildcard purge | Cost |
|---|---|---|---|---|
| Fastly | ~150ms | Yes (Surrogate-Key) | Yes | Free (unlimited) |
| Cloudflare | < 30s | Yes (Cache-Tag) | Yes | Free (included) |
| CloudFront | 5-15 min | No (use paths only) | Yes (with wildcards) | 1,000 free/month, then $0.005/path |
| Akamai | 5-7s (Fast Purge) | Yes (cache tags) | Yes | Depends on contract |
The purge API typically accepts one of three targets:
- Purge by URL: Invalidate one specific URL. Simple but tedious when one content change affects hundreds of pages.
- Purge by tag/surrogate key: Invalidate all objects tagged with a specific key. This is the surgical approach.
- Purge all: Nuclear option. Invalidate everything in the cache. Useful during deployment but causes a cache stampede.
Surrogate Keys: Surgical Invalidation at Scale
This is where the architecture gets interesting. Imagine you update a product's price on an e-commerce site. That price appears on the product detail page, the category listing page, the search results page, the homepage "featured products" section, and the shopping cart. That is 5+ different cached URLs that all need invalidation.
Purging by URL means you need to know every URL that contains this product's price. That is fragile and error-prone. Surrogate keys solve this by tagging cached content with metadata that identifies what entities it contains.
When the origin generates a response, it includes a header listing all the entities that response depends on:
Surrogate-Key: product-12345 category-electronics homepage-featured
Now, when product 12345's price changes, you send one purge command:
POST /purge/product-12345
The CDN finds every cached object tagged with product-12345 and invalidates all of them. One API call, surgical precision, zero need to enumerate URLs.
Notice that /products/67890 is not invalidated even though it is in the same cache. It does not carry the product-12345 tag, so the purge command ignores it. This is the surgical precision that surrogate keys provide.
The tradeoff: your application must explicitly tag every response with the right surrogate keys. This requires your CMS or rendering layer to track which entities contribute to each page. For a simple blog, this is easy (each article page has one tag). For a complex e-commerce site where a single page depends on products, inventory, pricing, promotions, and recommendations, the tagging logic can be complex.
Versioned URLs: Sidestep Invalidation Entirely
There is one strategy that eliminates the invalidation problem altogether for static assets: put a version identifier in the URL itself.
Instead of serving CSS at /styles/main.css, serve it at /styles/main.abc123.css where abc123 is a hash of the file contents. When the CSS changes, the hash changes, the URL changes, and the browser requests the new URL. The old URL stays cached (nobody requests it anymore), and the new URL has a cold cache that gets populated on first request.
This is what every modern build tool does. Webpack, Vite, and esbuild all produce content-hashed filenames by default.
The Cache-Control header for versioned assets is maximally aggressive:
Cache-Control: public, max-age=31536000, immutable
One year TTL, marked immutable. The CDN and browser will never revalidate this URL because the content at this URL will never change. If the content changes, the URL changes.
The limitation: this only works for content you reference by URL (scripts, stylesheets, images, fonts). It does not work for HTML pages because the URL is determined by the user typing it or following a link. You cannot change /blog/my-article to /blog/my-article-v2 every time you fix a typo. For HTML, you still need TTL + purge.
The key insight: use versioned URLs for all static assets (maximally long TTL, no invalidation needed) and TTL + active purge for HTML pages (shorter TTL, surgical invalidation when content changes). This combination covers 99% of real-world CDN caching needs.
The stale-while-revalidate Safety Net
There is one more directive that completes the picture. stale-while-revalidate tells the edge cache: "When this content expires, serve the stale version immediately and revalidate in the background."
Cache-Control: public, s-maxage=3600, stale-while-revalidate=60
This says: cache for 1 hour. After 1 hour, the content is stale. But for the next 60 seconds of staleness, serve the stale content to users instantly (zero latency) while asynchronously fetching the fresh version from the origin. After the background fetch completes, the cache is updated and subsequent users get the fresh version.
Without stale-while-revalidate, the first user after TTL expiry experiences the full revalidation latency (50-200ms depending on origin location). With it, that user still gets a fast response, just a slightly stale one.
This is especially valuable after a purge on a slow-propagating CDN like CloudFront. Even if the purge has not reached a particular edge PoP yet, the stale-while-revalidate window means users get served quickly while the cache updates in the background.
User A gets the stale content instantly. User B (arriving after the background revalidation completes) gets the fresh content. Nobody experiences a slow response. This is the ideal behavior for content where absolute real-time freshness is not critical but latency is.
The combination of all four strategies looks like this in practice:
| Content Type | TTL | Invalidation Strategy | stale-while-revalidate |
|---|---|---|---|
| Static assets (CSS, JS, images) | 1 year (immutable) | None needed (versioned URLs) | Not needed |
| HTML pages | 1-24 hours | Active purge + surrogate keys | 30-60 seconds |
| API responses (public) | 5-60 minutes | Active purge on data change | 10-30 seconds |
| API responses (user-specific) | Browser only (private) | Not CDN-cached | Not applicable |
| Real-time data | no-store | No caching | Not applicable |
Bottlenecks and failure modes
-
Cache stampede after a purge: When you purge a popular page, the next 10,000 concurrent requests all find an empty cache and all hit the origin simultaneously. This is a thundering herd problem that can overwhelm your origin. The solution is request coalescing (also called "request collapsing"): the CDN groups simultaneous cache misses for the same URL and sends only one request to the origin. All other requests wait for that one response, which is then cached and served to everyone. Fastly calls this "request collapsing." CloudFront does this at the shield layer.
-
Stale content during purge propagation: On CloudFront, a purge takes up to 15 minutes. During that window, some edge nodes have the old content and some have the new content. Users in Tokyo see the old headline while users in New York see the new one. There is no way to make this atomic across all 300+ PoPs. The mitigation is
stale-while-revalidate: the edge serves the stale content immediately while fetching the new version in the background. The user gets a fast response (stale), and the next user gets the fresh response. -
Multi-object consistency: If your page update involves changing both the HTML and a CSS file, the CDN might serve the new HTML with the old CSS (or vice versa) during the propagation window. Versioned URLs solve this for assets (the new HTML references the new CSS URL), but for multi-page updates (like a site-wide template change), there is no way to guarantee all pages flip atomically.
-
Purge storms during deployments: A deployment that changes templates across 50,000 pages triggers 50,000 purge commands. CDN providers rate-limit purge APIs. Fastly handles this gracefully with bulk purge support and surrogate keys. CloudFront throttles invalidation requests to 3,000 concurrent paths. You may need to batch purges or use "purge all" as a pragmatic alternative during major deploys.
-
Debugging cache behavior: When users report stale content, you need to determine whether the staleness is at the CDN edge, the shield, the browser cache, or an intermediate proxy. Each layer has different headers for inspecting cache status. Look for
X-Cache: HITorCF-Cache-Status: HITin response headers to identify which layer served the response. TheAgeheader tells you how long the response has been in the cache.
Common mistakes
| Mistake | What they say | Why it is wrong | What to say instead |
|---|---|---|---|
| Purge is instant | "Just purge and it updates everywhere" | CloudFront takes 5-15 minutes. Even Fastly's "instant" purge has a propagation window | "Purge speed varies by provider. Plan for the propagation gap with stale-while-revalidate" |
| Short TTLs everywhere | "Set max-age=60 to keep things fresh" | Destroys cache hit rate. Origin sees 300x more traffic | "Long TTLs for performance, active purge for freshness. Different content types get different TTLs" |
| Ignoring multi-tier caches | "The CDN has caches at the edge" | Modern CDNs have 2-3 cache tiers: edge, shield, and sometimes regional | "Invalidation must cascade through all tiers: edge, shield, origin" |
| No stampede prevention | "Purge the popular page" | 10,000 concurrent misses hit origin simultaneously | "Use request coalescing so only one miss reaches origin. Other requests wait for the response" |
| Versioned URLs for everything | "Hash every URL so we never invalidate" | Cannot version HTML pages because users bookmark and link to them | "Versioned URLs for static assets, TTL + purge for HTML pages" |
Practical checklist
- Classify content by freshness, personalization, and rollback needs before choosing a cache policy.
- Use long TTLs and immutable, content-hashed URLs for static assets; use shorter TTLs or active invalidation for HTML and public data.
- Decide whether a change invalidates one URL, an entity tag, a fragment, or an entire release; avoid broad purges when a narrower key works.
- Document the propagation window for every cache tier and provider, and expose purge request, completion, and failure metrics.
- Prevent stampedes with request coalescing, stale-while-revalidate, shield caches, bounded warming, and origin rate limits.
- Keep personalized or sensitive responses private; do not place user-specific content in a shared cache without a safe cache key and
Varypolicy. - Use validators such as
ETagandLast-Modifiedto reduce transfer when revalidation finds unchanged content. - Test deployment rollback, multi-object consistency, purge storms, browser-cache behavior, and stale-content incident response.
Test Your Understanding
Quick Recap
- CDN cache invalidation is a distributed consistency problem: propagating changes from one origin to 300+ edge replicas with minimal delay.
- TTL-based expiry is the passive baseline. Set TTLs via
Cache-Controlheaders: long for static assets, shorter for dynamic content. - Active purge forces immediate invalidation. Speed varies by provider: Fastly (~150ms), Cloudflare (< 30s), CloudFront (5-15 minutes).
- Surrogate keys enable surgical invalidation by tagging cached content with entity IDs, then purging by tag instead of by URL.
- Versioned URLs (content hashing in filenames) eliminate the need for invalidation entirely for static assets.
- Request coalescing prevents cache stampedes after purge by collapsing simultaneous cache misses into a single origin request.
stale-while-revalidateserves the old content immediately while fetching fresh content in the background, eliminating latency spikes during revalidation.- In practice, combine all strategies: versioned URLs for assets, long TTLs on HTML with active purge, surrogate keys for entity-level invalidation, and stale-while-revalidate as a safety net.
Related Concepts
- HTTP caching headers (Cache-Control, ETag, Vary): The protocol-level foundation of CDN caching. Understanding these headers is prerequisite for any CDN invalidation discussion.
- Distributed consistency models: CDN cache invalidation is an eventual consistency problem. The same tradeoffs between consistency and availability apply here as in distributed databases.
- Content delivery network architecture: Understanding the edge-shield-origin hierarchy is essential for reasoning about where caches sit and how invalidation propagates.
- Rate limiting and traffic shaping: Purge storms and cache stampedes are traffic spike problems. The same principles used in API rate limiting apply to managing origin load during invalidation events.
- Event-driven architectures: Surrogate key purging is conceptually similar to event-driven systems. A content change event triggers downstream cache invalidation, much like a domain event triggers downstream service updates.