Introduction
CDN cache invalidation not working occurs when purge requests fail to remove stale content from edge nodes, causing users to receive outdated assets despite invalidation requests. This error manifests as purged content still serving from cache, invalidation requests stuck in "InProgress" or "Failed" status, wildcard purges not matching expected paths, cache TTL not respecting purge commands, or certain edge locations not receiving invalidation signals. Common causes include invalidation API rate limits exceeded, cache keys not matching purge path (query string variations, hashing), origin Cache-Control headers overriding purge commands, CDN configuration with long TTL ignoring invalidation, wildcard path syntax incorrect, purge credentials insufficient permissions, CDN caching at multiple layers (edge, regional, origin shield), browser or ISP cache interfering with validation, and some CDN providers requiring additional propagation time for global invalidation. The fix requires understanding CDN cache hierarchies, proper invalidation API usage, cache key matching, and verification techniques. This guide provides production-proven troubleshooting for cache invalidation across AWS CloudFront, Cloudflare, Akamai, Fastly, and other major CDN providers.
Symptoms
- Purge request succeeds but content still cached
InvalidationInProgressbut old content still served- Wildcard purge
/*doesn't clear all files - Some edge locations return stale content
- Invalidation API returns 403 Forbidden or 400 Bad Request
Too Many Invaluationsrate limit error- Cache hits continue after purge (no cache miss spike)
- Query string variations not invalidated
- Mobile/AMP cached versions not cleared
- Regional cache persists after global purge
Common Causes
- Invalidation path doesn't match cached object keys
- Query string parameters create unique cache keys
- CDN configured with long TTL that ignores purges
- Rate limiting on invalidation API (e.g., CloudFront 1000 paths/hour)
- Wildcard syntax incorrect for CDN provider
- Multi-layer caching (edge + shield + origin)
- Origin server sending immutable Cache-Control headers
- Browser/service worker cache not CDN cache
- DNS propagation masking cache status
- CDN provider-specific delay (up to 15 minutes for global propagation)
Step-by-Step Fix
### 1. Diagnose cache invalidation status
Check invalidation progress:
```bash # AWS CloudFront - List invalidations aws cloudfront list-invalidations --distribution-id E1234567890ABC # Output: # { # "InvalidationList": { # "Items": [ # { # "Id": "I1A2B3C4D5E6F7", # "Status": "InProgress", # Or "Completed" # "CreateTime": "2024-01-15T10:00:00.000Z", # "InvalidationBatch": { # "Paths": { # "Items": ["/images/*", "/css/style.css"] # } # } # } # ] # } # }
# CloudFront - Get specific invalidation status aws cloudfront get-invalidation \ --distribution-id E1234567890ABC \ --id I1A2B3C4D5E6F7
# Cloudflare - Check purge status curl -X POST "https://api.cloudflare.com/v4/zones/ZONE_ID/purge_cache" \ -H "Authorization: Bearer API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"files": ["https://example.com/image.jpg"]}'
# Response: # { # "success": true, # "result": { "id": "purge_123" } # }
# Akamai - Check purge status (requires Akamai CLI) akamai purge list-requests akamai purge get-status REQUEST_ID ```
Verify cache hit/miss:
```bash # Check if request is served from cache # CloudFront - Look for X-Cache header curl -I https://d123.cloudfront.net/image.jpg # Headers: # X-Cache: Hit from cloudfront # Still cached # X-Cache: Miss from cloudfront # Fresh from origin
# Cloudflare - Look for CF-Cache-Status curl -I https://example.com/image.jpg # Headers: # CF-Cache-Status: HIT # Cached # CF-Cache-Status: MISS # Not cached # CF-Cache-Status: DYNAMIC # Not cacheable # CF-Cache-Status: EXPIRED # Was cached, now stale
# Fastly - Look for X-Cache header curl -I https://example.com/image.jpg # Headers: # X-Cache: HIT # X-Cache: MISS # X-Served-By: cache-lax8220-LAX # Shows which edge served
# Add cache-busting query param to test origin curl -I "https://example.com/image.jpg?v=$(date +%s)" ```
Check cached object details:
```bash # CloudFront - Check object in cache (requires Lambda@Edge) # Add Lambda@Edge on Origin Response to log cache status
# Cloudflare - Use Cache Analytics (Enterprise) curl "https://api.cloudflare.com/v4/zones/ZONE_ID/analytics/dashboard" \ -H "Authorization: Bearer API_TOKEN"
# Fastly - Real-time analytics curl "https://api.fastly.com/service/SERVICE_ID/stats/realtime" \ -H "Fastly-Key: API_TOKEN"
# Akamai - Use Log Delivery Service # Configure logs to include cache status ```
### 2. Fix invalidation path matching
Correct path syntax per CDN:
```bash # AWS CloudFront - Invalidation paths # Must start with / # Wildcards only at end of path
# CORRECT paths: /aws cloudfront create-invalidation \ --distribution-id E1234567890ABC \ --paths "/*" # Everything
aws cloudfront create-invalidation \ --distribution-id E1234567890ABC \ --paths "/images/*" # All files in /images/
aws cloudfront create-invalidation \ --distribution-id E1234567890ABC \ --paths "/images/logo.png" # Single file
# INCORRECT paths (won't work): # "images/*" # Missing leading / # "/images/*.png" # Wildcard only allowed at end # "**/images/*" # Double wildcard not supported
# Cloudflare - Purge by URL # Full URL including protocol and hostname curl -X POST "https://api.cloudflare.com/v4/zones/ZONE_ID/purge_cache" \ -H "Authorization: Bearer API_TOKEN" \ -d '{ "files": [ "https://example.com/images/logo.png", "https://www.example.com/style.css" ] }'
# Cloudflare - Purge by prefix (Enterprise) curl -X POST "https://api.cloudflare.com/v4/zones/ZONE_ID/purge_cache" \ -H "Authorization: Bearer API_TOKEN" \ -d '{"prefixes": ["https://example.com/images/"]}'
# Cloudflare - Purge by host (all URLs for domain) curl -X POST "https://api.cloudflare.com/v4/zones/ZONE_ID/purge_cache" \ -H "Authorization: Bearer API_TOKEN" \ -d '{"hosts": ["www.example.com"]}'
# Akamai - Purge by URL # URLs must be exact match to how they're cached akamai purge invalidate \ --file urls.txt \ --network production
# urls.txt content: # https://www.example.com/path/file.css # https://www.example.com/images/* ```
Handle query string cache keys:
```bash # Problem: CDN caches different query strings as separate objects # /image.jpg?version=1 and /image.jpg?version=2 are different cache keys
# CloudFront - Check query string forwarding config aws cloudfront get-distribution-config --id E1234567890ABC \ | jq '.DistributionConfig.CacheBehaviors[].QueryString'
# If QueryString=false, query strings ignored for cache key # But purge must match the cached object exactly
# Solution 1: Invalidate all query string variations aws cloudfront create-invalidation \ --distribution-id E1234567890ABC \ --paths "/image.jpg" # Invalidates base object
# Solution 2: Use query string forwarding # Update distribution to forward query strings # Then purge includes query strings
# Solution 3: Version files instead of query params # /image.v1.jpg, /image.v2.jpg # Each is separate object ```
### 3. Fix rate limiting and quota issues
CloudFront invalidation limits:
```bash # CloudFront limits: # - 1000 paths per invalidation request # - Unlimited invalidation requests, but each has cost ($0.005 per path) # - Wildcard /* counts as 1 path
# Batch large invalidations # Split into multiple requests if >1000 paths
# Example: Invalidate 5000 specific files # Split into 5 requests of 1000 paths each for i in {1..5}; do start=$((($i - 1) * 1000)) end=$(($i * 1000))
paths=$(cat files.txt | sed -n "${start},${end}p" | jq -R . | jq -s .)
aws cloudfront create-invalidation \ --distribution-id E1234567890ABC \ --paths "$paths"
sleep 1 # Avoid API rate limiting done
# Cost optimization: Use wildcards when possible # /* = $0.005 (1 path) # /images/*.png = $0.005 (1 path) # /images/a.png, /images/b.png, ... = $0.005 × N paths ```
Cloudflare purge limits:
```bash # Cloudflare limits (vary by plan): # Free: 3 purges per 5 minutes # Pro: 30 purges per 5 minutes # Business: 200 purges per 5 minutes # Enterprise: Custom limits
# Handle rate limit errors purge_with_retry() { local max_retries=5 local retry=0
while [ $retry -lt $max_retries ]; do response=$(curl -s -w "%{http_code}" -o /tmp/purge_result.json \ -X POST "https://api.cloudflare.com/v4/zones/ZONE_ID/purge_cache" \ -H "Authorization: Bearer API_TOKEN" \ -d "{\"files\": [\"$1\"]}")
if [ "$response" = "200" ]; then echo "Purge successful" return 0 elif [ "$response" = "429" ]; then retry=$((retry + 1)) wait_time=$((2 ** retry)) # Exponential backoff echo "Rate limited, retrying in ${wait_time}s..." sleep $wait_time else echo "Purge failed: $response" cat /tmp/purge_result.json return 1 fi done
echo "Max retries exceeded" return 1 }
# Purge everything (single request, counts as 1) curl -X POST "https://api.cloudflare.com/v4/zones/ZONE_ID/purge_cache" \ -H "Authorization: Bearer API_TOKEN" \ -d '{"purge_everything": true}' ```
Akamai purge limits:
```bash # Akamai limits: # - CCU (Cache Control Utility): 50,000 URLs per hour # - CPC (Cache Purge Console): Higher limits with Enterprise
# Check current usage akamai purge list-requests --status pending
# Estimate wait time for large purge # URLs / 50000 = hours to complete
# Use tags for efficient purge (Akamai Property Manager) # Tag URLs during caching, purge by tag later akamai purge invalidate \ --tag "production-assets" \ --network production ```
### 4. Fix TTL and Cache-Control conflicts
Override origin Cache-Control:
```bash # Problem: Origin sends long Cache-Control TTL # CDN may ignore purge if TTL still valid
# Example origin headers: # Cache-Control: public, max-age=86400 # 24 hours
# Solution 1: Override at CDN level # CloudFront - Cache behavior settings aws cloudfront update-distribution \ --id E1234567890ABC \ --distribution-config 'file://cache-config.json'
# cache-config.json: { "DefaultCacheBehavior": { "MinTTL": 0, "DefaultTTL": 3600, "MaxTTL": 86400, "ForwardedValues": { "QueryString": false, "Headers": { "Quantity": 1, "Items": ["Cache-Control"] } } } }
# Cloudflare - Page Rules to override curl -X POST "https://api.cloudflare.com/v4/zones/ZONE_ID/page_rules" \ -H "Authorization: Bearer API_TOKEN" \ -d '{ "targets": [{ "target": "url", "constraint": { "operator": "matches", "value": "example.com/assets/*" } }], "actions": [{ "id": "cache_level", "value": "cache_everything" }, { "id": "edge_cache_ttl", "value": 3600 }] }'
# Solution 2: Fix origin headers # Update origin to send appropriate Cache-Control
# Nginx origin: location /assets/ { # Shorter TTL for easy invalidation add_header Cache-Control "public, max-age=3600"; add_header "CDN-Cache-Control" "max-age=86400"; # For CDNs that respect it }
# Apache origin: <FilesMatch "\.(jpg|jpeg|png|gif|css|js)$"> Header set Cache-Control "max-age=3600, public" </FilesMatch> ```
Use immutable pattern for versioned assets:
```bash # Best practice: Version assets, use immutable caching # /static/app.v1.2.3.js # Hash in filename
# Origin headers for versioned assets: # Cache-Control: public, max-age=31536000, immutable
# Invalidate by changing filename, not purging cache # Old: /static/app.v1.2.3.js # New: /static/app.v1.2.4.js
# Build tool configuration:
# Webpack module.exports = { output: { filename: '[name].[contenthash].js', # Hash changes with content chunkFilename: '[name].[contenthash].chunk.js', } };
# This way, no purge needed - new hash = new cache entry ```
### 5. Fix multi-layer caching
Handle edge + regional cache:
```bash # Some CDNs have multiple cache layers # Edge (POP) -> Regional/Shield -> Origin
# CloudFront - Regional edge caches # Invalidation goes to all layers automatically # But may take longer to propagate
# Fastly - Shielding configuration # Check if shielding is enabled curl "https://api.fastly.com/service/SERVICE_ID/version/1/shield" \ -H "Fastly-Key: API_TOKEN"
# If shielding enabled, purge must clear both layers curl -X PURGE "https://example.com/image.jpg" \ -H "Fastly-Soft-Purge: 1" # Soft purge first
# Then hard purge curl -X PURGE "https://example.com/image.jpg"
# Akamai - Parent/Child caching # Use Akamai Property Manager to configure # Purge propagates from child to parent automatically ```
Origin shield considerations:
```bash # CloudFront - Origin Shield (additional caching layer) aws cloudfront get-distribution-config --id E1234567890ABC \ | jq '.DistributionConfig.OriginGroups'
# If Origin Shield enabled, invalidation clears: # 1. Edge locations # 2. Regional edge caches # 3. Origin Shield (if configured)
# Wait time may be longer with shield # Monitor with: aws cloudfront get-invalidation --id I123 --distribution-id E123
# Cloudflare - Argo Smart Routing # May cache at additional locations # Purge still clears all Cloudflare caches ```
### 6. Fix browser and intermediate cache
Browser cache vs CDN cache:
```bash # Even after CDN purge, browser may show old content
# Check browser cache # Chrome DevTools > Network > Disable Cache (for testing) # Or Ctrl+Shift+R / Cmd+Shift+R for hard refresh
# Force browser to revalidate # Origin headers: # Cache-Control: no-cache # Always revalidate with server # ETag: "abc123" # Entity tag for validation # Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
# Service worker cache # If using service worker, must clear its cache too
# JavaScript to unregister service worker: if ('serviceWorker' in navigator) { navigator.serviceWorker.getRegistrations().then(registrations => { registrations.forEach(reg => reg.unregister()); });
// Clear all caches caches.keys().then(names => { names.forEach(name => caches.delete(name)); }); } ```
ISP and recursive resolver cache:
```bash # Some ISPs cache DNS and/or HTTP content # Cannot control ISP caching
# Workarounds: # 1. Use cache-busting query strings for critical updates curl -I "https://example.com/update.css?v=20240115"
# 2. Wait for TTL expiration (usually < 1 hour)
# 3. Use different hostname for new content # old-cdn.example.com -> New content on new-cdn.example.com
# Test from multiple locations # Use tools like: # - curl from different networks # - webpagetest.org # - gtmetrix.com ```
### 7. Automate invalidation workflows
CI/CD integration:
```yaml # GitHub Actions - CloudFront invalidation name: Deploy on: push
jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Deploy to S3
- run: aws s3 sync ./dist s3://my-bucket/
- name: Invalidate CloudFront
- run: |
- # Invalidate changed files only
- CHANGED_FILES=$(git diff --name-only HEAD~1 | \
- sed 's|^|/|' | jq -R . | jq -s .)
aws cloudfront create-invalidation \ --distribution-id ${{ secrets.CF_DISTRIBUTION_ID }} \ --paths "$CHANGED_FILES" "/*" # Fallback full purge
# Or invalidate specific paths - name: Full invalidation run: | aws cloudfront create-invalidation \ --distribution-id ${{ secrets.CF_DISTRIBUTION_ID }} \ --paths "/*"
# GitHub Actions - Cloudflare invalidation name: Deploy jobs: deploy: steps: - name: Purge Cloudflare cache run: | curl -X POST "https://api.cloudflare.com/v4/zones/${{ secrets.CF_ZONE_ID }}/purge_cache" \ -H "Authorization: Bearer ${{ secrets.CF_API_TOKEN }}" \ -H "Content-Type: application/json" \ -d '{"purge_everything": true}' ```
Terraform invalidation:
```hcl # Terraform - CloudFront invalidation resource resource "aws_cloudfront_invalidation" "deploy" { distribution_id = aws_cloudfront_distribution.main.id paths = ["/*"]
# Trigger invalidation on S3 changes depends_on = [aws_s3_object_upload.assets] }
# Note: Terraform invalidation is synchronous (waits for completion) # For large invalidations, use awscli in provisioner instead resource "null_resource" "cf_invalidate" { provisioner "local-exec" { command = <<-EOT aws cloudfront create-invalidation \ --distribution-id ${aws_cloudfront_distribution.main.id} \ --paths "/*" \ --query 'Invalidation.Id' \ --output text EOT } } ```
Prevention
- Use versioned filenames (contenthash) to avoid manual purges
- Set appropriate Cache-Control TTL at origin
- Implement automated invalidation in CI/CD pipeline
- Monitor invalidation status with alerting
- Document invalidation procedures for each CDN provider
- Test invalidation as part of deployment checklist
- Use wildcard purges (/*) for full-site updates
- Consider cache tags for selective invalidation (Enterprise features)
- Implement cache-busting for critical hotfixes
- Verify purge completion before announcing deployments
Related Errors
- **CDN 502 Bad Gateway**: Origin server unreachable
- **CDN 503 Service Unavailable**: All origin servers unhealthy
- **CDN 403 Forbidden**: Origin access identity misconfigured
- **CDN SSL certificate expired**: HTTPS failing at edge
- **CDN CORS policy error**: Cross-origin requests blocked