CDN Caching: Global Content Distribution at Scale
Cache Invalidation at CDN: The Hardest Problem in Caching
Read aloud is unavailable in this browser.
The Two Hard Things in Computer Science
"There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton
This famous quote contains more truth than humor. While caching dramatically improves performance, cache invalidation—the process of removing or updating cached content when the source changes—is notoriously difficult to get right.
Consider this scenario: Your e-commerce site caches product pages for 1 hour to reduce origin load. A product's price changes from $99 to $79 for a flash sale. For the next hour, users see the old price. Some add to cart expecting $99 but get charged $79 (a pleasant surprise). Others see $79 on cached pages but $99 at checkout (rage and cart abandonment). Neither experience is acceptable.
Cache invalidation solves this: when content changes, you notify the CDN to remove or refresh cached copies immediately, ensuring users see correct content.
But at global scale with hundreds of edge locations and millions of cached objects, this becomes remarkably complex.
By the end of this page, you will understand the complete landscape of CDN cache invalidation—from basic purge operations to sophisticated tag-based invalidation, soft purge strategies, and architectural patterns that make invalidation reliable and efficient. You'll be equipped to design invalidation strategies for systems where content freshness is as critical as performance.
The Invalidation Problem at Scale
CDN cache invalidation seems simple in principle: tell the CDN "the content at this URL has changed." In practice, several factors complicate this:
Scale Challenges:
-
Geographic Distribution: A major CDN operates 200+ PoPs across 100+ countries. Invalidation must propagate to all locations storing the content.
-
Object Volume: A content-heavy site may have millions of unique cached URLs. Invalidating "all product images" could mean millions of individual cache entries.
-
Propagation Time: Invalidation isn't instantaneous. Even with efficient systems, propagation to all edge locations takes 1-30 seconds depending on CDN and method.
-
Timing Windows: Between triggering invalidation and completion, some users may still receive stale content. This creates race conditions in update scenarios.
-
Dependencies: Invalidating a product may require invalidating the product page, category pages showing that product, search results, homepage features, etc.
| Approach | Mechanism | Speed | Complexity | Best For |
|---|---|---|---|---|
| URL Purge | Delete specific URL from cache | Fast (2-5s) | Low | Known, specific URL updates |
| Prefix Purge | Delete all URLs matching prefix | Medium (5-15s) | Low | Section-wide invalidation |
| Tag-Based Purge | Delete all URLs with specific tag | Fast (2-5s) | Medium | Content relationship invalidation |
| Global Purge | Empty entire cache | Slow (30s+) | Very Low | Catastrophic recovery only |
| TTL Expiration | Wait for cache to expire naturally | TTL-dependent | None | Non-urgent updates |
| Soft Purge | Mark stale, serve while revalidating | Instant | Medium | High-traffic content updates |
Most CDNs charge for purge operations or limit them. Cloudflare's free tier allows 1,000 purge API calls per day. Akamai charges per purge request. Designing systems that require thousands of daily purges will hit cost and rate limits. Design invalidation strategies that minimize purge volume.
URL-Based Invalidation
The most straightforward invalidation approach: specify exact URLs to remove from cache.
Single URL Purge:
When a specific resource changes, purge its exact URL:
POST /purge
{
"url": "https://cdn.example.com/products/shoes-xyz"
}
The CDN removes this URL from all edge servers. Next request fetches fresh content from origin.
Batch URL Purge:
When multiple specific URLs change, purge in batch:
POST /purge
{
"urls": [
"https://cdn.example.com/products/shoes-xyz",
"https://cdn.example.com/products/shoes-abc",
"https://cdn.example.com/category/footwear"
]
}
URL Limitations:
-
Cache Key Complexity: If cache key includes query params or headers, you must purge all variations:
/products/shoes(base)/products/shoes?color=red(variant)/products/shoes?locale=fr(locale)
-
Unknown Variations: You may not know all cached URL variations if cache keys include dynamic elements.
-
Relationship Blind: URL purge doesn't understand content relationships. Updating a product requires manually identifying all URLs displaying that product.
1234567891011121314151617181920
# Cloudflare - Single URL Purgecurl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \ -H "Authorization: Bearer {api_token}" \ -H "Content-Type: application/json" \ --data '{"files":["https://cdn.example.com/products/shoes-xyz"]}' # Fastly - URL Purgecurl -X PURGE "https://cdn.example.com/products/shoes-xyz" \ -H "Fastly-Key: {api_token}" # AWS CloudFront - Create Invalidationaws cloudfront create-invalidation \ --distribution-id EDFDVBD6EXAMPLE \ --paths "/products/shoes-xyz" # Akamai - CCU APIcurl -X POST "https://api.akamai.net/ccu/v3/invalidate/url/production" \ -H "Authorization: EdgeGrid ..." \ -H "Content-Type: application/json" \ --data '{"objects":["https://cdn.example.com/products/shoes-xyz"]}'When many related URLs change, use prefix or wildcard purge instead of listing every URL. Purging /products/shoes* removes all shoe product pages in one request. Most CDNs support wildcards in purge paths, though syntax varies (CloudFront uses /path/*, Fastly uses surrogate keys, etc.).
Tag-Based Invalidation (Surrogate Keys)
Tag-based invalidation (also called surrogate keys on Fastly or cache tags on Cloudflare) is the most powerful invalidation mechanism. Instead of purging by URL, you purge by logical tags attached to cached responses.
How It Works:
-
Tagging: When origin responds, include headers specifying tags:
Surrogate-Key: product-123 category-footwear homepage-featured Cache-Tag: product-123 category-footwear homepage-featured -
CDN Stores Tags: The CDN associates these tags with the cached response.
-
Tag-Based Purge: When product 123 changes, purge by tag:
POST /purge { "tags": ["product-123"] } -
All Tagged Content Purged: Every cached response tagged with "product-123" is invalidated—including the product page, category pages showing this product, search results, etc.
Tag Strategy Design:
| Tag Type | Example | Use Case |
|---|---|---|
| Entity-based | product-123, user-456 | Invalidate when specific entity changes |
| Type-based | type-product, type-user | Invalidate all products (e.g., schema change) |
| Collection-based | category-shoes, search-results | Invalidate related collections |
| Template-based | template-pdp, template-homepage | Invalidate when page template changes |
| Version-based | api-v2, schema-2023-01 | Invalidate when API version changes |
| Time-based | hour-2024010112 | Logical grouping for time-based invalidation |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
// Express middleware to add surrogate keys to responsesfunction addSurrogateKeys(req, res, next) { const keys = new Set(); // Original response.json to intercept const originalJson = res.json.bind(res); res.json = function(body) { // Add entity keys based on response content if (body.product) { keys.add(`product-${body.product.id}`); keys.add(`category-${body.product.categoryId}`); if (body.product.brand) { keys.add(`brand-${body.product.brand.id}`); } } if (body.products && Array.isArray(body.products)) { body.products.forEach(p => keys.add(`product-${p.id}`)); keys.add('type-product-list'); } // Add page-level key keys.add(`page-${req.path.replace(/\//g, '-')}`); // Set Surrogate-Key header (Fastly) or Cache-Tag (Cloudflare) if (keys.size > 0) { res.set('Surrogate-Key', Array.from(keys).join(' ')); res.set('Cache-Tag', Array.from(keys).join(',')); } return originalJson(body); }; next();} // Purge by surrogate key when product updatesasync function onProductUpdate(productId) { // Fastly purge by surrogate key await fetch('https://api.fastly.com/service/{service_id}/purge/product-' + productId, { method: 'POST', headers: { 'Fastly-Key': process.env.FASTLY_API_KEY, } }); console.log(`Purged all content tagged with product-${productId}`);}Tag-Based Invalidation Benefits:
-
Relationship-Aware: One purge request invalidates all content showing a particular entity, regardless of URL structure.
-
Future-Proof: New pages displaying a product automatically get invalidated if properly tagged—no code changes needed.
-
Efficient: Purging one tag can invalidate thousands of URLs in a single API call.
-
Decoupled: Content authors don't need to know URL structures; they just specify entity identifiers.
Tag Implementation Considerations:
- •Limit Tag Count: Most CDNs limit tags per response (Fastly: ~16KB header size). Keep tags focused and relevant.
- •Normalize Tag Format: Use consistent naming conventions (lowercase, hyphens, prefixed by type).
- •Include Hierarchy: Tag both specific (
product-123) and general (type-product) for flexible invalidation. - •Avoid Over-Tagging: Don't tag every possible relationship—focus on relationships that actually trigger invalidation.
- •Test Tag Coverage: Verify all pages displaying an entity are properly tagged before relying on tag-based invalidation.
Fastly has the most mature surrogate key implementation. Cloudflare supports Cache-Tag on Enterprise plans. CloudFront requires alternative approaches (edge functions or path-based invalidation). Verify your CDN's tag support before designing tag-based invalidation strategies.
Soft Purge and Stale-While-Revalidate
Traditional purge ("hard purge") removes content entirely—subsequent requests must wait for origin fetch. Soft purge marks content as stale but continues serving it while asynchronously revalidating.
Hard Purge vs Soft Purge:
- •Content immediately removed from cache
- •Next request waits for origin fetch
- •Latency spike for first requests after purge
- •Origin sees immediate load spike
- •Risk of origin overload if content is popular
- •Content marked as stale, not removed
- •Stale content served immediately
- •Background revalidation fetches fresh content
- •No latency spike for users
- •Gradual origin load as content revalidates
How Soft Purge Works:
-
Soft Purge Request: CDN marks targeted content as stale but keeps it in cache.
-
First Request After Purge: CDN serves stale content immediately, triggers background revalidation.
-
Background Revalidation: CDN fetches fresh content from origin asynchronously.
-
Cache Update: Once fresh content arrives, cache is updated. Subsequent requests get fresh content.
-
Fallback Behavior: If origin fails during revalidation, CDN can continue serving stale content (stale-if-error).
Soft Purge Implementation:
123456789101112
# Fastly - Soft Purge by URLcurl -X PURGE "https://cdn.example.com/products/shoes-xyz" \ -H "Fastly-Key: {api_token}" \ -H "Fastly-Soft-Purge: 1" # Fastly - Soft Purge by Surrogate Keycurl -X POST "https://api.fastly.com/service/{service_id}/purge/product-123" \ -H "Fastly-Key: {api_token}" \ -H "Fastly-Soft-Purge: 1" # The Fastly-Soft-Purge: 1 header changes behavior from hard to soft purge# Content will be served stale while revalidating in backgroundWhen to Use Soft vs Hard Purge:
| Scenario | Recommended | Rationale |
|---|---|---|
| Routine content update | Soft Purge | Users see slightly stale content briefly; no latency impact |
| Security-critical change | Hard Purge | Cannot risk serving stale content (exposed secrets, etc.) |
| Price/inventory update | Soft Purge | Brief staleness acceptable; user experience preserved |
| Legal compliance removal | Hard Purge | Content must be removed immediately |
| High-traffic content | Soft Purge | Prevents origin overload thundering herd |
| Rarely-accessed content | Either | Low impact either way |
Stale-While-Revalidate Connection:
Soft purge and the stale-while-revalidate cache directive achieve similar outcomes:
- stale-while-revalidate: Automatic—content serves stale while revalidating when TTL expires
- Soft Purge: On-demand—you trigger stale serving + revalidation when content changes
Best practice: Configure stale-while-revalidate in cache headers AND use soft purge for updates. This provides resilience for both scheduled TTL expiration and on-demand invalidation.
Unless you have specific requirements for immediate content removal, soft purge should be your default invalidation method. It maintains user experience, prevents origin thundering herds, and provides graceful degradation if origin is unavailable. Save hard purge for security-critical scenarios.
Invalidation Propagation and Timing
When you issue a purge request, how long until it takes effect globally? Understanding propagation timing is critical for applications where invalidation must coordinate with other operations.
Propagation Phases:
- API Acceptance: CDN receives and validates purge request (milliseconds)
- Command Distribution: Purge command sent to all edge locations (1-10 seconds)
- Local Processing: Each edge executes purge against local cache (milliseconds)
- Confirmation: CDN confirms invalidation complete (varies by CDN)
CDN Propagation Comparison:
| CDN | URL Purge | Tag-Based Purge | Prefix/Wildcard | Notes |
|---|---|---|---|---|
| Fastly | ~1-2 seconds | ~1-2 seconds | ~150ms (instant purge) | Industry-leading speed |
| Cloudflare | ~30 seconds | ~30 seconds (Enterprise) | ~30 seconds | Uses eventually consistent propagation |
| CloudFront | ~60-120 seconds | N/A | ~60-120 seconds | Charges per invalidation path; slower |
| Akamai | ~5-10 seconds | ~5-10 seconds | ~5-10 seconds | Fast CCU (Content Control Utility) |
Timing Implications:
Database Update → CDN Refresh Race Condition:
Consider this sequence:
- Database updated with new product price at T+0
- Purge request sent at T+0
- User requests product page at T+1 (purge not yet propagated)
- CDN serves cached content with old price
- CDN fetches from origin at T+1 (origin returns old price due to replication lag)
- Purge completes at T+2
- Cache now contains... old price!
The race condition between purge propagation and database replication can result in caching stale content even after invalidation.
Mitigation Strategies:
- Delay Purge: Wait for database replication to complete before purging
- Version/Timestamp Check: Origin includes version in response; CDN validates freshness
- Two-Phase Invalidation: Purge once immediately, again after replication delay
- Cache-Buster Query Params: Include version in URL to bypass stale cache
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// Pattern: Delayed invalidation to handle replication lagasync function updateProduct(productId, newData) { // 1. Update database (primary) await db.products.update(productId, newData); // 2. Wait for replication to complete // Option A: Fixed delay (simple but not precise) await delay(500); // 500ms replication buffer // Option B: Verify replication (more robust) await waitForReplication(productId, newData.version); // 3. Now safe to invalidate await cdnPurge(`product-${productId}`);} // Alternative: Double-purge patternasync function updateProductWithDoublePurge(productId, newData) { await db.products.update(productId, newData); // Immediate purge (catches some in-flight requests) await cdnPurge(`product-${productId}`); // Delayed secondary purge (catches replication race) setTimeout(() => { cdnPurge(`product-${productId}`); }, 1000);} // Verify function for replication checkasync function waitForReplication(productId, expectedVersion) { const maxAttempts = 10; const delayMs = 100; for (let i = 0; i < maxAttempts; i++) { const product = await db.products.findById(productId, { replica: true }); if (product.version >= expectedVersion) { return; } await delay(delayMs); } throw new Error('Replication timeout');}Even with fast CDNs, invalidation is not instantaneous. Design your systems to tolerate brief windows of staleness. For truly atomic updates, consider versioned URLs (content-addressed) rather than relying solely on invalidation timing.
Invalidation Patterns and Architecture
How you architect invalidation within your system significantly impacts reliability (will invalidations always happen?), performance (how fast is the feedback loop?), and maintainability (can developers understand and debug the system?).
Pattern 1: Synchronous Inline Invalidation
Invalidate as part of the update transaction:
Transaction {
update database
call CDN purge API
if purge fails, rollback or retry
commit
}
✅ Guarantees invalidation happens with update ❌ Adds latency to every update operation ❌ CDN API failures can block updates
Pattern 2: Asynchronous Queue-Based Invalidation
Decouple invalidation from update:
Update {
update database
publish InvalidationEvent to queue
commit
}
Invalidation Worker {
consume InvalidationEvent
call CDN purge API
retry on failure
}
✅ Updates are fast; invalidation is async ✅ Retries handle transient CDN failures ❌ Brief window between update and invalidation ❌ Additional infrastructure (queue, workers)
Pattern 3: Change Data Capture (CDC) Based Invalidation
Use database CDC to trigger invalidation:
Database Change Stream → CDC → Invalidation Service → CDN Purge
✅ Application code doesn't need invalidation logic ✅ Captures ALL database changes (including direct SQL) ✅ Enables complex invalidation rules based on data changes ❌ Requires CDC infrastructure (Debezium, etc.) ❌ Slight delay from change to CDC event
Pattern 4: Webhook-Driven Invalidation (CMS/Headless)
For CMS-managed content, use CMS webhooks:
Content Update in CMS → Webhook → Invalidation Service → CDN Purge
✅ Content editors trigger immediate invalidation ✅ No custom application code required ✅ Works with any webhook-capable CMS ❌ Depends on CMS webhook reliability
Choosing the Right Pattern:
| Pattern | Best For | Complexity | Latency |
|---|---|---|---|
| Synchronous | Critical real-time updates; low volume | Low | High (inline) |
| Async Queue | General purpose; high volume | Medium | Low (decoupled) |
| CDC-Based | Complex rules; database-centric | High | Low (async) |
| Webhook | CMS content; third-party integrations | Low | Medium |
For most applications, async queue-based invalidation provides the best balance of reliability, performance, and maintainability. It decouples your application from CDN API availability, handles retries gracefully, and scales well. Start here unless you have specific requirements that demand a different approach.
Invalidation Monitoring and Debugging
How do you know if invalidation is working? When users report stale content, how do you diagnose the issue? Robust monitoring and debugging capabilities are essential for maintaining confidence in your invalidation system.
Key Metrics to Track:
- •Purge Request Volume — Track purges per minute/hour. Sudden spikes may indicate runaway invalidation or cascading updates.
- •Purge Latency (API Response) — Time from purge request to CDN acknowledgment. Degradation indicates CDN issues.
- •Propagation Time — Time from purge request to confirmed invalidation at all edges. Monitor against CDN SLA.
- •Purge Error Rate — Failed purge requests. Non-zero rates require investigation and retry logic.
- •Cache Hit Rate — Sudden drops may indicate over-invalidation or configuration issues.
- •Stale Content Reports — User-reported stale content incidents. The ultimate measure of invalidation effectiveness.
Debugging Stale Content Reports:
When users report seeing outdated content:
Step 1: Verify the Report
- Get specific URL and timestamp
- Check what content they saw vs. expected
- Confirm whether content has actually updated at origin
Step 2: Check CDN Cache Status
- Request URL with cache debug header (varies by CDN)
- Check
Ageheader to determine when cached - Check
X-Cacheor equivalent to confirm HIT/MISS - Verify cache key matches expected
Step 3: Review Invalidation Logs
- Was an invalidation triggered for this content?
- What time was it triggered vs. user report?
- Did invalidation complete successfully?
Step 4: Test Invalidation Flow
- Trigger test invalidation for problematic URL
- Measure propagation across multiple locations
- Verify content is fresh after invalidation
Step 5: Root Cause Analysis
- Race condition (replication lag)?
- Invalidation not triggered (bug in trigger logic)?
- Wrong cache key (URL vs. actual cache key mismatch)?
- CDN configuration issue (cache rules changed)?")
CDN Debug Headers:
| CDN | Cache Status Header | Cache Age | Special Debug |
|---|---|---|---|
| Cloudflare | cf-cache-status (HIT/MISS/EXPIRED) | Age | CF-Ray for request tracing |
| Fastly | X-Cache (HIT/MISS) | Age | Fastly-Debug-Digest for key/TTL |
| CloudFront | X-Cache (Hit/Miss from cloudfront) | Age | X-Amz-Cf-Id for request ID |
| Akamai | X-Cache (TCP_HIT/TCP_MISS) | X-Cache-Key | Pragma: akamai-x-cache-on |
Create operational dashboards showing invalidation volume, latency, and error rates in real-time. When stale content incidents occur, you should be able to correlate with invalidation events within minutes. Proactive monitoring catches issues before users report them.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.