Loading content...
Phil Karlton famously declared that "There are only two hard things in Computer Science: cache invalidation and naming things." While often quoted humorously, this statement encapsulates a profound truth about distributed systems. Caching is straightforward—you store data closer to where it's needed. But determining when and how to remove stale data from caches distributed across hundreds of global locations? That's where the complexity explodes.
In a Content Delivery Network with edge nodes spanning every continent, a single content update can require coordinated invalidation across thousands of cache servers, each with its own state, network conditions, and timing constraints. This is the domain of purge requests—the fundamental mechanism through which you command a CDN to forget what it has cached.
By the end of this page, you will understand the complete lifecycle of CDN purge requests, from API invocation to edge node execution. You'll learn the different purge mechanisms, their performance characteristics, rate limiting considerations, and how to architect systems that invalidate caches reliably at massive scale.
A purge request is an explicit instruction to a CDN to remove specific cached content from its edge servers before the content's natural TTL (Time-To-Live) expiration. Without purge capabilities, you would be entirely at the mercy of TTL timers—a situation that becomes untenable when content accuracy is business-critical.
Why TTL Alone Is Insufficient:
Consider a news organization publishing a breaking story. The initial article contains an error—perhaps an incorrect casualty figure or a misattributed quote. The content is cached across 200 edge locations with a 1-hour TTL. Without purge capabilities:
Purge requests solve this by allowing immediate, targeted invalidation independent of TTL schedules.
A URL like /api/products/123 might generate different cache keys based on Accept-Language, User-Agent, cookies, or query parameters. Effective purging requires understanding exactly how your CDN constructs cache keys—otherwise, you might purge one variant while stale copies of other variants persist.
Understanding the internal mechanics of purge request processing illuminates why certain strategies perform better than others and helps diagnose invalidation failures.
The Purge Request Lifecycle:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
// Initiating a purge requestconst purgeResponse = await cdnClient.purge({ // Target specific URLs for precise invalidation urls: [ 'https://cdn.example.com/images/hero-banner.jpg', 'https://cdn.example.com/api/products/featured', 'https://cdn.example.com/css/main.css' ], // Optional: Target specific geographic regions regions: ['us-east', 'eu-west', 'ap-southeast'], // Request type: 'hard' removes immediately, 'soft' marks stale purgeType: 'hard'}); // Response structureconsole.log(purgeResponse);/*{ purgeId: 'prg-8f4a2c1e-3b7d-4e5f-9a6b-1c2d3e4f5a6b', status: 'in_progress', requestedAt: '2024-01-15T10:30:00.000Z', estimatedCompletionMs: 5000, targetCount: 3, affectedEdgeNodes: 147, trackingUrl: 'https://api.cdn.com/purge/prg-8f4a2c1e-3b7d-4e5f-9a6b-1c2d3e4f5a6b'}*/ // Polling for completion (with exponential backoff)async function waitForPurgeCompletion(purgeId: string, maxWaitMs: number = 30000) { const startTime = Date.now(); let backoffMs = 500; while (Date.now() - startTime < maxWaitMs) { const status = await cdnClient.getPurgeStatus(purgeId); if (status.status === 'completed') { return { success: true, completedAt: status.completedAt }; } if (status.status === 'failed') { throw new Error(`Purge failed: ${status.errorMessage}`); } // Exponential backoff with jitter await sleep(backoffMs + Math.random() * 100); backoffMs = Math.min(backoffMs * 2, 5000); } throw new Error('Purge completion timeout exceeded');}Even CDN APIs that return '200 OK' for purge requests are acknowledging receipt, not completion. Always implement polling or webhook-based verification when purge timing is critical to your application logic.
CDN providers offer multiple ways to specify what content to purge. Each approach has distinct performance characteristics, cost implications, and use cases. Choosing the right targeting strategy is critical for operational efficiency.
| Targeting Method | Scope | Speed | Cost | Best Use Case |
|---|---|---|---|---|
| Single URL Purge | Exact URL match | Fastest (1-5 seconds) | Low cost | Individual file updates, critical fixes |
| Batch URL Purge | List of specific URLs | Fast (5-15 seconds) | Moderate | Coordinated multi-file deployments |
| Wildcard Purge | URL pattern matching | Medium (15-60 seconds) | Higher | Directory invalidation, prefix-based content |
| Tag-Based Purge | Logical content groups | Medium (10-30 seconds) | Moderate | Content type invalidation, entity updates |
| Full Cache Purge | All cached content | Slow (minutes) | Very high | Major deployments, emergency situations only |
Single URL Purges: Precision Invalidation
Single URL purges target one specific cache key. This is the most efficient approach when you know exactly what changed:
PURGE: https://cdn.example.com/images/product-12345.jpg
The CDN directly invalidates this specific entry across all edge nodes. There's no pattern matching overhead, making this the fastest purge type.
Wildcard Purges: Pattern-Based Invalidation
Wildcard purges use glob-style patterns to match multiple URLs:
PURGE: https://cdn.example.com/images/product-*
PURGE: https://cdn.example.com/api/users/*/profile
This is powerful but computationally expensive. The CDN must evaluate every cached entry against the pattern, which scales with total cache size rather than match count.
In production systems, prefer precise single-URL purges for routine operations and reserve wildcard purges for exceptional cases. Maintain a manifest of cached URLs during deployment so you can issue targeted purges rather than broad wildcards.
Every CDN provider enforces rate limits on purge requests. These limits exist to protect the CDN's control infrastructure and ensure fair resource allocation across customers. Understanding and designing around these limits is essential for high-volume invalidation scenarios.
Typical Rate Limit Structures:
| Provider | URL Purges | Wildcard Purges | Cool-down Period |
|---|---|---|---|
| CloudFlare (Free) | 1,000/day | 1/month | None |
| CloudFlare (Pro) | 10,000/day | Unlimited | None |
| CloudFlare (Enterprise) | 500,000/day | Unlimited | None |
| AWS CloudFront | 1,000 paths/purge, 3,000/month free | Counts as number of files matched | None |
| Fastly | Unlimited | Unlimited | Rate-limited by API quotas |
| Akamai | 50,000/day (varies by contract) | Varies | Sliding window |
Designing for Rate Limit Compliance:
When your application requires frequent cache invalidation, you must architect around rate limits rather than fighting them:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
interface PurgeRequest { urls: string[]; priority: 'critical' | 'high' | 'normal' | 'low'; requestedAt: Date; retryCount: number;} class RateLimitedPurgeQueue { private queue: PriorityQueue<PurgeRequest>; private rateLimiter: SlidingWindowRateLimiter; private batchSize: number; constructor( maxPurgesPerMinute: number = 100, maxPurgesPerDay: number = 10000, batchSize: number = 50 ) { this.queue = new PriorityQueue<PurgeRequest>( (a, b) => this.comparePriority(a, b) ); this.rateLimiter = new SlidingWindowRateLimiter({ perMinute: maxPurgesPerMinute, perDay: maxPurgesPerDay }); this.batchSize = batchSize; } async enqueue(urls: string[], priority: PurgeRequest['priority'] = 'normal') { // Deduplicate URLs before queuing const uniqueUrls = [...new Set(urls)]; // Split large requests into batches for (let i = 0; i < uniqueUrls.length; i += this.batchSize) { const batch = uniqueUrls.slice(i, i + this.batchSize); this.queue.enqueue({ urls: batch, priority, requestedAt: new Date(), retryCount: 0 }); } this.processQueue(); } private async processQueue() { while (!this.queue.isEmpty()) { // Check rate limits before dequeuing const tokensAvailable = await this.rateLimiter.checkAvailability(); if (tokensAvailable === 0) { const waitTime = this.rateLimiter.getNextAvailableTime(); await this.scheduleResumeIn(waitTime); return; } const request = this.queue.dequeue(); try { await this.executePurge(request); await this.rateLimiter.consume(request.urls.length); } catch (error) { if (this.isRateLimitError(error)) { // Re-queue with backoff request.retryCount++; if (request.retryCount < 3) { this.queue.enqueue(request); } else { this.logPurgeFailed(request, error); } } } } } private comparePriority(a: PurgeRequest, b: PurgeRequest): number { const priorityOrder = { critical: 0, high: 1, normal: 2, low: 3 }; const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority]; if (priorityDiff !== 0) return priorityDiff; // Within same priority, FIFO return a.requestedAt.getTime() - b.requestedAt.getTime(); }}If you exhaust your purge quota during a critical content update, you lose the ability to invalidate until the limit resets. Monitor quota consumption as a key operational metric, and alert when usage exceeds 70% of daily limits.
Purge requests can fail silently. A successful API response indicates the CDN accepted your request, not that content was actually invalidated across all edge nodes. Robust systems verify purge effectiveness through multiple mechanisms.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
interface PurgeVerificationResult { purgeId: string; verified: boolean; verificationMethod: string; sampledRegions: RegionVerification[]; totalDurationMs: number;} interface RegionVerification { region: string; verified: boolean; cacheStatus: 'HIT' | 'MISS' | 'EXPIRED' | 'STALE'; ageHeader: number | null; contentHash: string;} async function verifyPurgeWithMultiRegionSampling( purgeId: string, targetUrl: string, expectedContentHash: string, regions: string[] = ['us-east', 'us-west', 'eu-west', 'ap-southeast']): Promise<PurgeVerificationResult> { const startTime = Date.now(); // First, wait for CDN to report purge completion await waitForPurgeCompletion(purgeId); // Then verify from multiple regions const verifications = await Promise.all( regions.map(async (region) => { const response = await fetchFromRegion(targetUrl, region); const headers = response.headers; const body = await response.text(); const actualHash = computeHash(body); return { region, verified: actualHash === expectedContentHash, cacheStatus: parseCacheStatus(headers.get('X-Cache')), ageHeader: parseInt(headers.get('Age') || '0'), contentHash: actualHash }; }) ); const allVerified = verifications.every(v => v.verified); // Log for observability metrics.recordPurgeVerification({ purgeId, success: allVerified, regionsSuccess: verifications.filter(v => v.verified).length, regionsTotal: verifications.length, durationMs: Date.now() - startTime }); if (!allVerified) { const failedRegions = verifications.filter(v => !v.verified); alerting.warn('Purge verification failed for regions', { purgeId, failedRegions: failedRegions.map(r => r.region) }); } return { purgeId, verified: allVerified, verificationMethod: 'multi-region-content-hash', sampledRegions: verifications, totalDurationMs: Date.now() - startTime };}Track p50, p95, and p99 purge completion times. If p99 suddenly spikes, it may indicate CDN infrastructure issues, network partitions, or approaching rate limits. These metrics are leading indicators of content freshness problems.
Years of operational experience across high-traffic systems have distilled key practices that separate reliable purge operations from those that cause incidents.
Anti-Patterns to Avoid:
High-volume applications require architectural patterns that abstract purge complexity while ensuring reliability and efficiency.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
// Event-driven purge architecture using message queue// This decouples content updates from purge execution interface ContentUpdateEvent { eventId: string; contentType: 'product' | 'article' | 'image' | 'config'; entityId: string; action: 'create' | 'update' | 'delete'; timestamp: Date; metadata: Record<string, string>;} class PurgeEventProcessor { constructor( private urlResolver: ContentUrlResolver, private purgeClient: CDNPurgeClient, private deduplicator: PurgeDeduplicator ) {} async processEvent(event: ContentUpdateEvent): Promise<void> { // 1. Resolve content event to affected cache URLs const affectedUrls = await this.urlResolver.resolveUrls(event); // 2. Deduplicate against recently purged URLs const urlsToPurge = await this.deduplicator.filter(affectedUrls); if (urlsToPurge.length === 0) { return; // Already purged recently } // 3. Determine purge priority based on content type const priority = this.determinePriority(event); // 4. Issue purge with appropriate strategy const purgeId = await this.purgeClient.purge({ urls: urlsToPurge, priority, metadata: { triggerEvent: event.eventId, contentType: event.contentType } }); // 5. Track for deduplication window (typically 60s) await this.deduplicator.markPurged(urlsToPurge, 60); // 6. Emit purge initiated event for downstream processing await this.emitPurgeEvent({ purgeId, urls: urlsToPurge, triggerEvent: event.eventId, status: 'initiated' }); } private determinePriority(event: ContentUpdateEvent): string { // Critical: breaking news, security fixes // High: product info, pricing // Normal: regular content updates // Low: non-time-sensitive updates if (event.metadata.urgent === 'true') return 'critical'; if (event.contentType === 'product') return 'high'; return 'normal'; }} // URL Resolution: Maps content changes to cache URLsclass ContentUrlResolver { async resolveUrls(event: ContentUpdateEvent): Promise<string[]> { const urls: string[] = []; switch (event.contentType) { case 'product': // Product detail page urls.push(`${CDN_BASE}/products/${event.entityId}`); // Product API endpoint urls.push(`${CDN_BASE}/api/v1/products/${event.entityId}`); // Related collection pages (resolved via database) const collections = await this.getProductCollections(event.entityId); collections.forEach(c => { urls.push(`${CDN_BASE}/collections/${c.slug}`); }); break; case 'article': urls.push(`${CDN_BASE}/articles/${event.entityId}`); urls.push(`${CDN_BASE}/api/articles/${event.entityId}`); // Invalidate article listing pages urls.push(`${CDN_BASE}/articles`); break; // ... other content types } return urls; }}When multiple events affect the same content (e.g., rapid edits), deduplication ensures you don't waste purge quota on redundant invalidations. A 60-second deduplication window is usually sufficient while still ensuring timely updates.
Purge requests are the foundation of CDN cache invalidation—the mechanism through which you take control of content freshness across globally distributed edge networks.
What's Next:
With foundational purge mechanics understood, the next page explores Soft Purge vs Hard Purge—the critical distinction between marking content stale and removing it entirely, and how this choice affects origin load, user experience, and system resilience.
You now understand CDN purge request fundamentals—the lifecycle, targeting strategies, rate limiting, verification, and architectural patterns for reliable cache invalidation at scale.