Introduction
Your CDN (Content Delivery Network) is not properly purging cached content, causing updated files, API responses, or web pages to remain stale across edge locations. Users see outdated content even after you've made changes to your origin server. Cache purge commands seem to execute but the content doesn't update, or updates take much longer than expected to propagate globally.
This impacts user experience with outdated content, prevents deployments from taking effect, causes issues with dynamic content being cached incorrectly, and leads to users seeing old versions of JavaScript, CSS, images, or API data. The problem can originate from CDN configuration issues, improper cache headers, stale-while-revalidate settings, network propagation delays, or incorrect purge API usage.
Symptoms
``` # User complaints "I still see the old version of the website after you said it was updated" "The new feature isn't showing up for me" "I'm getting old API data"
# CDN purge errors {"success": false, "errors": [{"code": 10000, "message": "Invalid purge request"}]}
Error: Purge rate limit exceeded. Please wait before submitting another purge request.
HTTP 429 Too Many Requests - Rate limit exceeded for cache purge operations
Purge request timeout - The purge operation took too long to complete
# In CDN dashboard Purge Status: Pending (for extended periods) Purge Status: Failed Cache Hit Rate: Unchanged after purge
# curl tests showing stale content $ curl -I https://example.com/style.css Cache-Control: max-age=31536000 Last-Modified: Mon, 01 Jan 2024 00:00:00 GMT # Old date!
# Origin server shows new file $ curl -I https://origin.example.com/style.css Last-Modified: Tue, 08 Apr 2026 17:30:00 GMT # New date! ```
Common Causes
- 1.Insufficient cache control headers: Origin server sends
Cache-Control: max-age=31536000(1 year) without proper revalidation, causing CDN to cache content indefinitely. - 2.Stale-while-revalidate misconfiguration: CDN serves stale content while checking origin, but revalidation never completes or origin returns unchanged responses.
- 3.Purge propagation delays: Global CDN with many edge locations requires time to propagate purge commands. Large purges (many URLs or wildcards) take longer.
- 4.Rate limiting on purge operations: CDN providers limit purge requests per minute/hour. Exceeding limits causes purge failures or delays.
- 5.Incorrect purge method: Using single URL purge for wildcard needs, or using soft purge when hard purge is required.
- 6.Cache key mismatch: CDN caches based on specific key (URL + headers + query params). Purging one key doesn't invalidate others.
- 7.Vary header issues: Content varies by User-Agent or other headers, creating multiple cached versions that aren't all purged.
- 8.Query string handling: CDN ignores query strings for caching, so
style.css?v=2serves the same cachedstyle.css. - 9.Origin shield/caching layer: An intermediate caching layer between CDN and origin holds stale content that wasn't purged.
- 10.DNS and routing issues: DNS changes or routing updates haven't propagated, causing requests to hit old CDN nodes or origins.
Step-by-Step Fix
Step 1: Verify Current CDN Cache State
Diagnose what's actually cached:
```bash # Check cache headers from different endpoints curl -I https://example.com/static/style.css curl -I https://origin.example.com/static/style.css
# Check with cache bypass curl -I -H "Cache-Control: no-cache" https://example.com/static/style.css
# Check specific CDN POP locations (if available) # Cloudflare - use different endpoints curl -I https://example.com/cdn-cgi/trace
# Check X-Cache headers (varies by CDN) # Cloudflare: cf-cache-status # Fastly: X-Served-By, X-Cache # Akamai: X-Cache-Key
# Cloudflare cache status meanings: # HIT - served from cache # MISS - not in cache, fetched from origin # EXPIRED - was cached but TTL expired # STALE - serving stale while revalidating # BYPASS - cache bypassed # REVALIDATED - revalidated with origin
# Check Last-Modified and ETag curl -sI https://example.com/style.css | grep -E "Last-Modified|ETag|Cache-Control|Age|cf-cache-status"
# Compare with origin curl -sI https://origin.example.com/style.css | grep -E "Last-Modified|ETag|Cache-Control"
# Check if content actually differs diff <(curl -s https://example.com/style.css) <(curl -s https://origin.example.com/style.css)
# Check multiple locations (if you have access to VPNs or different regions) # US East curl -I https://example.com/style.css --resolve example.com:443:104.16.132.229 # Europe curl -I https://example.com/style.css --resolve example.com:443:104.16.133.229
# Check DNS to see which CDN nodes are serving dig example.com nslookup example.com ```
Script to check cache status:
```bash #!/bin/bash # check_cdn_cache.sh
URL=${1:-"https://example.com"} ORIGIN=${2:-"https://origin.example.com"}
echo "=== CDN Cache Check ===" echo "CDN URL: $URL" echo "Origin URL: $ORIGIN" echo ""
echo "--- CDN Response ---" curl -sI "$URL" | grep -E "HTTP|Cache|Last-Modified|ETag|Age|cf-cache-status|X-Cache"
echo "" echo "--- Origin Response ---" curl -sI "$ORIGIN" | grep -E "HTTP|Cache|Last-Modified|ETag"
echo "" echo "--- Content Comparison ---" CDN_HASH=$(curl -s "$URL" | md5sum | cut -d' ' -f1) ORIGIN_HASH=$(curl -s "$ORIGIN" | md5sum | cut -d' ' -f1)
echo "CDN content hash: $CDN_HASH" echo "Origin content hash: $ORIGIN_HASH"
if [ "$CDN_HASH" = "$ORIGIN_HASH" ]; then echo "✓ Content matches" else echo "✗ Content differs - CDN has stale content!" fi ```
Step 2: Fix Cache Control Headers
Properly configure origin server cache headers:
```nginx # Nginx origin configuration
# Static assets - long cache with versioning in URL location /static/ { alias /var/www/static/;
# Cache for 1 year with immutable add_header Cache-Control "public, max-age=31536000, immutable";
# Enable ETag for revalidation etag on;
# Set Last-Modified if_modified_since exact; }
# Assets with versioning via query string location /assets/ { alias /var/www/assets/;
# Cache for 1 year add_header Cache-Control "public, max-age=31536000";
# Ignore query string for caching (CDN setting) # But use version in filename instead: app.v123.js }
# Dynamic content - short cache or no cache location /api/ { add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; add_header Pragma "no-cache"; add_header Expires "0";
# Vary header for different response types add_header Vary "Accept, Authorization"; }
# HTML - short cache with revalidation location ~ \.html$ { add_header Cache-Control "public, max-age=300, s-maxage=600, stale-while-revalidate=60"; etag on; }
# Images - medium cache location ~ \.(jpg|jpeg|png|gif|webp|ico)$ { add_header Cache-Control "public, max-age=86400, stale-while-revalidate=3600"; etag on; }
# CSS/JS - version in filename, long cache location ~ \.(css|js)$ { add_header Cache-Control "public, max-age=31536000, immutable"; etag on; }
# Don't cache admin or user-specific pages location /admin/ { add_header Cache-Control "private, no-store, no-cache, must-revalidate"; }
# Fonts - long cache location ~ \.(woff2?|ttf|otf|eot)$ { add_header Cache-Control "public, max-age=31536000, immutable"; add_header Access-Control-Allow-Origin "*"; } ```
Apache configuration:
```apache # .htaccess or httpd.conf
# Enable mod_expires and mod_headers <IfModule mod_expires.c> ExpiresActive On
# Images ExpiresByType image/jpeg "access plus 1 day" ExpiresByType image/png "access plus 1 day" ExpiresByType image/gif "access plus 1 day" ExpiresByType image/webp "access plus 1 day" ExpiresByType image/x-icon "access plus 1 week"
# CSS and JavaScript ExpiresByType text/css "access plus 1 year" ExpiresByType application/javascript "access plus 1 year"
# Fonts ExpiresByType font/woff "access plus 1 year" ExpiresByType font/woff2 "access plus 1 year"
# HTML ExpiresByType text/html "access plus 5 minutes"
# No cache for dynamic content ExpiresByType application/json "access plus 0 seconds" </IfModule>
<IfModule mod_headers.c> # Cache-Control headers <FilesMatch "\.(css|js)$"> Header set Cache-Control "public, max-age=31536000, immutable" </FilesMatch>
<FilesMatch "\.(jpg|jpeg|png|gif|webp)$"> Header set Cache-Control "public, max-age=86400" </FilesMatch>
<FilesMatch "\.html$"> Header set Cache-Control "public, max-age=300, stale-while-revalidate=60" </FilesMatch>
# No cache for API <LocationMatch "^/api/"> Header set Cache-Control "no-store, no-cache, must-revalidate" Header set Pragma "no-cache" Header set Expires "0" </LocationMatch>
# Vary header Header append Vary "Accept-Encoding" </IfModule> ```
Step 3: Configure CDN-Specific Purge Settings
Set up proper purge configuration for your CDN:
```bash # Cloudflare
# Install Cloudflare CLI npm install -g wrangler # or use curl with API
# Get Zone ID curl -X GET "https://api.cloudflare.com/client/v4/zones?name=example.com" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json"
# Purge single file curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ --data '{"files":["https://example.com/style.css"]}'
# Purge multiple files curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ --data '{"files":["https://example.com/style.css", "https://example.com/app.js"]}'
# Purge by prefix (Enterprise only) curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ --data '{"prefixes":["https://example.com/static/"]}'
# Purge everything (use sparingly!) curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ --data '{"purge_everything":true}'
# Purge by cache tag (Enterprise) curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ --data '{"tags":["user-123", "product-456"]}'
# Purge by host curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ --data '{"hosts":["static.example.com"]}' ```
```bash # Fastly
# Install Fastly CLI # https://developer.fastly.com/reference/cli/
# Or use API directly
# Get service info curl "https://api.fastly.com/service/SERVICE_ID" \ -H "Fastly-Key: YOUR_API_KEY"
# Single URL purge (soft purge - marks as stale) curl -X PURGE "https://example.com/style.css" \ -H "Fastly-Key: YOUR_API_KEY" \ -H "Fastly-Soft-Purge: 1"
# Hard purge (immediate removal) curl -X PURGE "https://example.com/style.css" \ -H "Fastly-Key: YOUR_API_KEY"
# Purge by surrogate key curl -X POST "https://api.fastly.com/service/SERVICE_ID/purge" \ -H "Fastly-Key: YOUR_API_KEY" \ -H "Surrogate-Key: product-123"
# Purge all curl -X POST "https://api.fastly.com/service/SERVICE_ID/purge_all" \ -H "Fastly-Key: YOUR_API_KEY"
# Purge by URL with multiple headers curl -X POST "https://api.fastly.com/purge/example.com/style.css" \ -H "Fastly-Key: YOUR_API_KEY" \ -H "Accept: application/json" ```
```bash # Akamai
# Using Akamai CLI akamai purge --help
# Purge by URL akamai purge invalidate --url "https://example.com/style.css"
# Purge by CP code akamai purge invalidate --cpcode 123456
# Using {OPEN} API curl -X POST "https://akab-xxxxx-xxxxx.purge.akamaiapis.net/v3/purge/urls" \ -H "Authorization: EG1-HMAC-SHA256 ..." \ -H "Content-Type: application/json" \ -d '{ "objects": [ "https://example.com/style.css", "https://example.com/app.js" ] }'
# Invalidate by cache tag curl -X POST "https://akab-xxxxx-xxxxx.purge.akamaiapis.net/v3/purge/tags" \ -H "Authorization: EG1-HMAC-SHA256 ..." \ -H "Content-Type: application/json" \ -d '{ "objects": ["product-123", "user-456"] }' ```
Step 4: Implement Versioned Asset Strategy
Use asset versioning to avoid cache issues:
```html <!-- Method 1: Query string (less reliable) --> <link rel="stylesheet" href="/style.css?v=1.2.3"> <script src="/app.js?v=1.2.3"></script>
<!-- Method 2: Filename versioning (recommended) --> <link rel="stylesheet" href="/style.v1.2.3.css"> <script src="/app.v1.2.3.js"></script>
<!-- Method 3: Path versioning --> <link rel="stylesheet" href="/v1.2.3/style.css"> <script src="/v1.2.3/app.js"></script>
<!-- Method 4: Content hash (best) --> <link rel="stylesheet" href="/style.a1b2c3d4.css"> <script src="/app.e5f6g7h8.js"></script> ```
Build system for asset versioning:
```javascript // webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = { output: { filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist'), publicPath: '/static/', }, plugins: [ new HtmlWebpackPlugin({ template: 'src/index.html', filename: '../index.html', }), ], };
// Or using Vite // vite.config.js export default { build: { rollupOptions: { output: { entryFileNames: '[name].[hash].js', chunkFileNames: '[name].[hash].js', assetFileNames: '[name].[hash][extname]', }, }, }, };
// gulpfile.js for static sites const gulp = require('gulp'); const rev = require('gulp-rev'); const revRewrite = require('gulp-rev-rewrite');
gulp.task('revision', () => { return gulp.src(['dist//*.css', 'dist//*.js']) .pipe(rev()) .pipe(gulp.dest('dist')) .pipe(rev.manifest()) .pipe(gulp.dest('dist')); });
gulp.task('revrewrite', () => { const manifest = gulp.src('dist/rev-manifest.json');
return gulp.src('dist/*.html') .pipe(revRewrite({ manifest })) .pipe(gulp.dest('dist')); }); ```
Runtime asset version management:
```php <?php // Asset version helper class AssetVersion { private static $manifest = null;
public static function load() { if (self::$manifest === null) { $manifestPath = __DIR__ . '/dist/manifest.json'; if (file_exists($manifestPath)) { self::$manifest = json_decode(file_get_contents($manifestPath), true); } } }
public static function get($path) { self::load();
if (isset(self::$manifest[$path])) { return self::$manifest[$path]; }
// Fallback: add file modification time $fullPath = __DIR__ . '/public' . $path; if (file_exists($fullPath)) { $mtime = filemtime($fullPath); return $path . '?v=' . $mtime; }
return $path; }
public static function asset($path) { return '/static' . self::get($path); } }
// Usage in templates <link rel="stylesheet" href="<?= AssetVersion::asset('/style.css') ?>"> <script src="<?= AssetVersion::asset('/app.js') ?>"></script>
// Outputs: // <link rel="stylesheet" href="/static/style.a1b2c3d4.css"> // <script src="/static/app.e5f6g7h8.js"></script> ?> ```
Step 5: Configure Cache Keys and Vary Headers
Ensure cache keys match purge operations:
```nginx # Nginx - Configure cache key
# Cache key should include only relevant parts proxy_cache_key "$scheme$host$request_uri";
# Or with additional components proxy_cache_key "$scheme$host$request_uri$http_accept_encoding";
# Don't vary by User-Agent unless content actually varies # This creates cache fragmentation proxy_ignore_headers "Vary";
# Set proper Vary header from origin add_header Vary "Accept-Encoding"; ```
Cloudflare cache key configuration:
```javascript // Cloudflare Worker for custom cache key addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)); });
async function handleRequest(request) { const url = new URL(request.url);
// Create cache key without query params const cacheKey = new Request(url.origin + url.pathname, request);
const cache = caches.default;
// Check cache let response = await cache.match(cacheKey);
if (response) { return response; }
// Fetch from origin response = await fetch(request);
// Store in cache const headers = new Headers(response.headers); headers.set('Cache-Control', 'public, max-age=3600');
const cachedResponse = new Response(await response.clone().arrayBuffer(), { status: response.status, statusText: response.statusText, headers: headers });
event.waitUntil(cache.put(cacheKey, cachedResponse));
return response; } ```
Fastly VCL for cache key:
```vcl # Fastly VCL
# Custom cache key sub vcl_hash { hash_data(req.url);
# Include only if content varies if (req.http.Accept-Encoding) { hash_data(req.http.Accept-Encoding); }
# Don't include User-Agent unless necessary # hash_data(req.http.User-Agent);
return (lookup); }
# Purge handling sub vcl_recv { if (req.method == "PURGE") { if (!client.ip ~ purge_allowed) { return (synth(405, "PURGE not allowed")); } return (purge); }
# Ban (soft purge) if (req.method == "BAN") { ban("req.url ~ " + req.http.X-Ban-Url); return (synth(200, "Banned")); } } ```
Step 6: Implement Stale Content Strategy
Handle stale content gracefully:
```nginx # Nginx stale content handling
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m use_temp_path=off;
server { location / { proxy_cache my_cache;
# Serve stale content while revalidating proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
# Background update proxy_cache_background_update on;
# Lock while fetching proxy_cache_lock on;
# Revalidate with If-Modified-Since proxy_cache_revalidate on;
# Cache validity proxy_cache_valid 200 302 10m; proxy_cache_valid 404 1m;
# Add headers add_header X-Cache-Status $upstream_cache_status; } } ```
```php <?php // PHP application handling
// Set stale-while-revalidate headers function setCacheHeaders($maxAge = 300, $staleWhileRevalidate = 60) { header("Cache-Control: public, max-age=$maxAge, stale-while-revalidate=$staleWhileRevalidate"); header("Last-Modified: " . gmdate('D, d M Y H:i:s', filemtime(__FILE__)) . ' GMT'); header("ETag: " . md5_file(__FILE__));
// Check If-Modified-Since if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { $ifModifiedSince = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']); $lastModified = filemtime(__FILE__);
if ($lastModified <= $ifModifiedSince) { http_response_code(304); exit; } }
// Check If-None-Match (ETag) if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) { $etag = md5_file(__FILE__); if ($_SERVER['HTTP_IF_NONE_MATCH'] === $etag) { http_response_code(304); exit; } } }
// CDN surrogate keys for targeted purging function setSurrogateKeys($keys) { // Fastly header("Surrogate-Key: " . implode(' ', $keys));
// Cloudflare Enterprise header("Cache-Tag: " . implode(',', $keys));
// Akamai header("Edge-Cache-Tag: " . implode(',', $keys)); }
// Usage setCacheHeaders(300, 60); setSurrogateKeys(['product-123', 'category-electronics']);
// Output content echo $productData; ?> ```
Step 7: Automate Cache Purging in Deployment
Integrate purge into CI/CD pipeline:
```yaml # .github/workflows/deploy.yml
name: Deploy and Purge CDN
on: push: branches: [main]
jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3
- name: Build assets
- run: |
- npm ci
- npm run build
- name: Deploy to origin
- run: |
- rsync -avz --delete dist/ user@origin.example.com:/var/www/html/
- name: Purge Cloudflare cache
- env:
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
- run: |
- # Get list of changed files
- CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD | grep -E '\.(css|js|html)$' | sed 's|^|https://example.com/|')
if [ -n "$CHANGED_FILES" ]; then curl -X POST "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/purge_cache" \ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ -H "Content-Type: application/json" \ --data "{\"files\":$(echo "$CHANGED_FILES" | jq -R -s -c 'split("\n")[:-1]')}" fi
- name: Verify deployment
- run: |
- sleep 10 # Wait for purge to propagate
- curl -I https://example.com/style.css
`
```bash #!/bin/bash # scripts/purge_cdn.sh - Deployment purge script
CDN_PROVIDER=${CDN_PROVIDER:-"cloudflare"} DRY_RUN=${DRY_RUN:-false}
# Get changed files from git get_changed_files() { git diff --name-only HEAD~1 HEAD -- '*.css' '*.js' '*.html' '*.png' '*.jpg' '*.webp' }
# Purge Cloudflare purge_cloudflare() { local files=("$@")
if [ ${#files[@]} -eq 0 ]; then echo "No files to purge" return fi
# Build JSON array of URLs local urls="" for file in "${files[@]}"; do urls+="\"https://example.com/$file\"," done urls="[${urls%,}]"
if [ "$DRY_RUN" = true ]; then echo "Would purge: $urls" return fi
curl -X POST "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/purge_cache" \ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ -H "Content-Type: application/json" \ --data "{\"files\":$urls}" }
# Purge Fastly purge_fastly() { local files=("$@")
for file in "${files[@]}"; do if [ "$DRY_RUN" = true ]; then echo "Would purge: https://example.com/$file" else curl -X PURGE "https://example.com/$file" \ -H "Fastly-Key: $FASTLY_API_KEY" fi done }
# Purge Akamai purge_akamai() { local files=("$@")
local urls="" for file in "${files[@]}"; do urls+="\"https://example.com/$file\"," done urls="[${urls%,}]"
if [ "$DRY_RUN" = true ]; then echo "Would purge: $urls" return fi
# Akamai requires signed request akamai purge invalidate --json "{\"objects\":$urls}" }
# Main mapfile -t changed_files < <(get_changed_files)
if [ ${#changed_files[@]} -eq 0 ]; then echo "No changed files to purge" exit 0 fi
echo "Purging ${#changed_files[@]} files from $CDN_PROVIDER..."
case $CDN_PROVIDER in cloudflare) purge_cloudflare "${changed_files[@]}" ;; fastly) purge_fastly "${changed_files[@]}" ;; akamai) purge_akamai "${changed_files[@]}" ;; *) echo "Unknown CDN provider: $CDN_PROVIDER" exit 1 ;; esac ```
Step 8: Monitor and Alert on Cache Issues
Set up monitoring for cache problems:
```python #!/usr/bin/env python3 # cdn_cache_monitor.py
import requests import time import json from datetime import datetime
class CDNCacheMonitor: def __init__(self, urls, origin_url, alert_webhook=None): self.urls = urls self.origin_url = origin_url self.alert_webhook = alert_webhook
def check_cache_status(self, url): """Check cache status for a URL""" try: # Check CDN cdn_response = requests.head(url, timeout=10) cdn_data = { 'url': url, 'status': cdn_response.status_code, 'headers': dict(cdn_response.headers) }
# Check origin origin_url = url.replace('https://example.com', self.origin_url) origin_response = requests.head(origin_url, timeout=10) origin_data = { 'url': origin_url, 'status': origin_response.status_code, 'headers': dict(origin_response.headers) }
# Compare Last-Modified cdn_modified = cdn_response.headers.get('Last-Modified') origin_modified = origin_response.headers.get('Last-Modified')
is_stale = False if cdn_modified and origin_modified: if cdn_modified != origin_modified: is_stale = True
# Check cache header cache_status = cdn_response.headers.get('cf-cache-status', cdn_response.headers.get('X-Cache', 'Unknown'))
return { 'url': url, 'cdn_status': cdn_data['status'], 'origin_status': origin_data['status'], 'cache_status': cache_status, 'is_stale': is_stale, 'cdn_modified': cdn_modified, 'origin_modified': origin_modified, 'timestamp': datetime.now().isoformat() }
except Exception as e: return { 'url': url, 'error': str(e), 'timestamp': datetime.now().isoformat() }
def check_all(self): """Check all monitored URLs""" results = [] for url in self.urls: result = self.check_cache_status(url) results.append(result)
if result.get('is_stale'): self.send_alert(result)
return results
def send_alert(self, result): """Send alert for stale cache""" if not self.alert_webhook: return
message = { 'text': f"⚠️ Stale cache detected for {result['url']}", 'details': { 'cdn_modified': result['cdn_modified'], 'origin_modified': result['origin_modified'], 'cache_status': result['cache_status'] } }
try: requests.post(self.alert_webhook, json=message) except Exception as e: print(f"Failed to send alert: {e}")
def run_continuous(self, interval=300): """Run continuous monitoring""" while True: print(f"Checking cache status at {datetime.now()}") results = self.check_all()
# Log results for result in results: if result.get('is_stale'): print(f"STALE: {result['url']}") elif result.get('error'): print(f"ERROR: {result['url']} - {result['error']}") else: print(f"OK: {result['url']} - {result.get('cache_status')}")
time.sleep(interval)
if __name__ == '__main__': urls = [ 'https://example.com/style.css', 'https://example.com/app.js', 'https://example.com/index.html', ]
monitor = CDNCacheMonitor( urls=urls, origin_url='https://origin.example.com', alert_webhook='https://hooks.slack.com/services/YOUR/WEBHOOK' )
monitor.run_continuous(interval=300) ```
Step 9: Debug and Troubleshoot Purge Issues
Create debugging tools:
```bash #!/bin/bash # debug_cdn_cache.sh
URL=${1:-"https://example.com/style.css"} ORIGIN=${2:-"https://origin.example.com/style.css"}
echo "=== CDN Cache Debug ===" echo "URL: $URL" echo "Origin: $ORIGIN" echo ""
echo "1. DNS Resolution" echo "CDN IP:" dig +short $(echo $URL | sed 's|https\?://||' | cut -d'/' -f1) | head -5 echo ""
echo "2. SSL/TLS Info" echo | openssl s_client -connect $(echo $URL | sed 's|https\?://||' | cut -d'/' -f1):443 -servername $(echo $URL | sed 's|https\?://||' | cut -d'/' -f1) 2>/dev/null | openssl x509 -noout -dates echo ""
echo "3. CDN Response Headers" curl -sI "$URL" | grep -E "HTTP|Cache|Last-Modified|ETag|Age|cf-cache-status|X-Cache|Server|Date" echo ""
echo "4. Origin Response Headers" curl -sI "$ORIGIN" | grep -E "HTTP|Cache|Last-Modified|ETag|Date" echo ""
echo "5. Content Comparison" CDN_HASH=$(curl -s "$URL" | md5sum | cut -d' ' -f1) ORIGIN_HASH=$(curl -s "$ORIGIN" | md5sum | cut -d' ' -f1) echo "CDN content hash: $CDN_HASH" echo "Origin content hash: $ORIGIN_HASH"
if [ "$CDN_HASH" = "$ORIGIN_HASH" ]; then echo "✓ Content matches" else echo "✗ Content differs - CDN has stale content!"
echo "" echo "6. Testing with cache bypass" curl -sI -H "Cache-Control: no-cache" "$URL" | grep -E "Last-Modified|ETag|cf-cache-status"
echo "" echo "7. Testing from different endpoints" # Cloudflare curl -sI "$URL" --resolve example.com:443:104.16.132.229 | grep "Last-Modified" curl -sI "$URL" --resolve example.com:443:104.16.133.229 | grep "Last-Modified" fi
echo "" echo "8. Cache Control Analysis" curl -sI "$ORIGIN" | grep -i "cache-control" | while read line; do echo "Origin: $line" done
curl -sI "$URL" | grep -i "cache-control" | while read line; do echo "CDN: $line" done ```
Step 10: Implement Graceful Degradation
Handle cache failures gracefully:
```javascript // Frontend cache busting and fallback
class AssetLoader { constructor() { this.loadedAssets = new Map(); this.maxRetries = 3; this.retryDelay = 1000; }
async loadWithFallback(primaryUrl, fallbackUrl) {
try {
// Try primary with cache busting
const cacheBuster = ?_=${Date.now()};
const response = await fetch(primaryUrl + cacheBuster, {
cache: 'reload'
});
if (!response.ok) {
throw new Error(HTTP ${response.status});
}
return await response.text();
} catch (error) {
console.warn(Failed to load ${primaryUrl}:, error);
// Try fallback
if (fallbackUrl) {
console.log(Trying fallback: ${fallbackUrl});
const fallbackResponse = await fetch(fallbackUrl, {
cache: 'reload'
});
if (fallbackResponse.ok) { return await fallbackResponse.text(); } }
throw error; } }
async loadAsset(url, retries = 0) { try { const response = await fetch(url, { cache: 'no-store' });
if (!response.ok) {
throw new Error(HTTP ${response.status});
}
const content = await response.text(); this.loadedAssets.set(url, content); return content;
} catch (error) { if (retries < this.maxRetries) { await new Promise(r => setTimeout(r, this.retryDelay * (retries + 1))); return this.loadAsset(url, retries + 1); } throw error; } }
async loadStylesheet(url) { if (this.loadedAssets.has(url)) { return; }
const css = await this.loadAsset(url); const style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); }
async loadScript(url) { if (this.loadedAssets.has(url)) { return; }
const js = await this.loadAsset(url); const script = document.createElement('script'); script.textContent = js; document.body.appendChild(script); } }
// Usage const loader = new AssetLoader();
// Load with version check
async function loadWithVersionCheck(baseUrl, currentVersion) {
const manifestUrl = ${baseUrl}/manifest.json;
try { const manifest = await fetch(manifestUrl, { cache: 'no-store' }).then(r => r.json());
if (manifest.version !== currentVersion) {
console.log(New version available: ${manifest.version});
// Clear old caches, load new version
}
// Load assets from manifest
for (const [name, path] of Object.entries(manifest.assets)) {
await loader.loadAsset(${baseUrl}${path});
}
} catch (error) { console.error('Failed to load assets:', error); // Fallback to cached/default version } } ```
Prevention
| Step | Action | Verified |
|---|---|---|
| 1 | Verified current CDN cache state | ☐ |
| 2 | Fixed cache control headers | ☐ |
| 3 | Configured CDN-specific purge settings | ☐ |
| 4 | Implemented versioned asset strategy | ☐ |
| 5 | Configured cache keys and vary headers | ☐ |
| 6 | Implemented stale content strategy | ☐ |
| 7 | Automated cache purging in deployment | ☐ |
| 8 | Set up monitoring and alerts | ☐ |
| 9 | Created debugging tools | ☐ |
| 10 | Implemented graceful degradation | ☐ |
Verification
- 1.Test cache headers:
- 2.```bash
- 3.curl -I https://example.com/style.css | grep -i "cache|last-modified"
- 4.
` - 5.Test purge:
- 6.```bash
- 7.# Make change to file on origin
- 8.curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \
- 9.-H "Authorization: Bearer TOKEN" \
- 10.-H "Content-Type: application/json" \
- 11.--data '{"files":["https://example.com/style.css"]}'
- 12.
` - 13.Verify content updated:
- 14.```bash
- 15.sleep 10 # Wait for propagation
- 16.curl -sI https://example.com/style.css | grep "Last-Modified"
- 17.
`
Related Issues
- [Fix Nginx Reverse Proxy 502 Bad Gateway](/articles/fix-nginx-reverse-proxy-502-bad-gateway)
- [Fix Cloudflare 522 Connection Timed Out](/articles/fix-cloudflare-522-connection-timed-out)
- [Fix Fastly VCL Error](/articles/fix-fastly-vcl-error)
- [Fix Website Loading Slow CDN Issues](/articles/fix-website-loading-slow-cdn-issues)
- [Fix Cloudflare SSL Handshake Failed](/articles/fix-cloudflare-ssl-handshake-failed)
Related Articles
- [Technical troubleshooting: Fix Cdn 502 Bad Gateway Origin Server Unreachable ](cdn-502-bad-gateway-origin-server-unreachable-ssl-handshake)
- [Technical troubleshooting: Fix Cdn Bandwidth Spike Cost Overrun Issue in CDN](cdn-bandwidth-spike-cost-overrun)
- [Technical troubleshooting: Fix Cdn Cache Key Mixing Mobile Desktop Issue in C](cdn-cache-key-mixing-mobile-desktop)
- [Technical troubleshooting: Fix Cdn Cache Stale Content Not Purging Issue in C](cdn-cache-stale-content-not-purging)
- [Technical troubleshooting: Fix Cdn Compression Not Working Assets Issue in CD](cdn-compression-not-working-assets)
<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "TechArticle", "headline": "Fix CDN Cache Not Purging Updates Stuck", "description": "Learn how to fix CDN cache not purging when updates are stuck. Includes Cloudflare, Fastly, Akamai purge configuration and cache invalidation strategies.", "url": "https://www.fixwikihub.com/fix-cdn-cache-not-purging-updates-stuck", "publisher": { "@type": "Organization", "name": "FixWikiHub", "url": "https://www.fixwikihub.com" }, "author": { "@type": "Person", "name": "FixWikiHub Editorial Team" }, "datePublished": "2025-12-13T01:28:16.346Z", "dateModified": "2025-12-13T01:28:16.346Z" } </script>