CDN Cache, Invalidation and Performance: Technical Implementation
CDN cache stores copies of content on edge servers close to users, reducing latency and load on the origin server. Cache invalidation is the mechanism to update or remove that content before its natural expiration. The combination of effective caching and precise invalidation determines how fast and consistent your content is delivered.
Last updated: 2026-06-18
How CDN Cache Works
When a user requests content, the CDN checks if a copy exists at the nearest edge server. If it exists and is valid, it serves directly (cache hit). If it doesn’t exist or has expired, it fetches from origin and stores for future requests (cache miss).
┌─────────────────────────────────────────────────────────────────┐│ User Request │└───────────────────────────┬─────────────────────────────────────┘ │ ▼ ┌─────────────────┐ │ CDN Edge │ │ Cache Check │ └────────┬────────┘ │ ┌─────────────┴─────────────┐ │ │ ▼ ▼ ┌───────────┐ ┌───────────┐ │ Cache Hit │ │ Cache Miss│ │ (Serve) │ │ (Fetch) │ └─────┬─────┘ └─────┬─────┘ │ │ │ ▼ │ ┌───────────────┐ │ │ Origin │ │ │ Server │ │ └───────┬───────┘ │ │ │ ▼ │ ┌───────────────┐ │ │ Store in │ │ │ Edge Cache │ │ └───────┬───────┘ │ │ └───────────┬───────────────┘ │ ▼ ┌─────────────┐ │ Response │ │ to User │ └─────────────┘Cache Strategy Comparison
| Strategy | How It Works | When to Use |
|---|---|---|
| TTL (Time-to-Live) | Content expires after defined time | Static content, low change frequency |
| Stale-While-Revalidate | Serves stale, updates in background | High availability, stale tolerance |
| Stale-If-Error | Serves stale if origin fails | Resilience, fallback |
| Cache-Aside | Application manages cache explicitly | Dynamic APIs, full control |
| Write-Through | Updates cache on write | Critical data, immediate consistency |
HTTP Cache Control Headers
Cache-Control
| Directive | Meaning | Example Use |
|---|---|---|
max-age=3600 | Cache valid for 3600 seconds | Versioned assets |
s-maxage=3600 | TTL for CDN (different from browser) | CDN-specific |
public | Can be cached by anyone | Public content |
private | Browser cache only | User data |
no-cache | Revalidate before serving | Sensitive content |
no-store | Do not cache | Sensitive data |
stale-while-revalidate=86400 | Serve stale for 24h while revalidating | High availability |
stale-if-error=3600 | Serve stale if origin returns error | Resilience |
ETag and Last-Modified
Request: If-None-Match: "abc123" If-Modified-Since: Wed, 18 Jun 2026 10:00:00 GMT
Response (not modified): HTTP/1.1 304 Not Modified
Response (modified): HTTP/1.1 200 OK ETag: "def456" Last-Modified: Wed, 18 Jun 2026 12:00:00 GMT Cache-Control: max-age=3600Cache Configuration by Content Type
TTL Recommendations by Content Type
| Content Type | Recommended TTL | Cache-Control Example |
|---|---|---|
| Versioned static assets (JS, CSS, images) | 1 year (immutable) | public, max-age=31536000, immutable |
| Unversioned images | 30 days | public, max-age=2592000 |
| HTML pages (static) | 1-24 hours | public, max-age=3600, stale-while-revalidate=86400 |
| HTML pages (dynamic) | 0-5 minutes | public, max-age=300, stale-while-revalidate=3600 |
| API responses (public) | 1-60 minutes | public, max-age=60, s-maxage=300 |
| API responses (authenticated) | No cache or per-user | private, no-cache |
| Video/audio segments | 1 year (immutable) | public, max-age=31536000, immutable |
| Video manifests (HLS/DASH) | 1-10 seconds | public, max-age=10 |
| Fonts | 1 year | public, max-age=31536000, immutable |
| JSON data (rarely changes) | 1 hour | public, max-age=3600, stale-if-error=86400 |
Cache-Control Header Examples
Static assets with content hashing:
Cache-Control: public, max-age=31536000, immutableContent with hash in filename (e.g., app.a3f2b1c.js) should use immutable to prevent unnecessary revalidation.
HTML with stale fallback:
Cache-Control: public, max-age=300, stale-while-revalidate=3600, stale-if-error=86400HTML revalidates every 5 minutes, serves stale up to 1 hour, and falls back to stale if origin fails for 24 hours.
API with short TTL and stale tolerance:
Cache-Control: public, max-age=10, s-maxage=60, stale-while-revalidate=300Browser revalidates every 10 seconds, CDN caches for 60 seconds, and serves stale for 5 minutes during revalidation.
Nginx Origin Server Configuration
Static files:
server { listen 80; server_name static.example.com;
location /assets/ { root /var/www; expires 1y; add_header Cache-Control "public, max-age=31536000, immutable"; add_header Surrogate-Key "static-assets"; }
location /images/ { root /var/www; expires 30d; add_header Cache-Control "public, max-age=2592000"; try_files $uri @origin; }}Dynamic content with surrogate keys:
server { listen 80; server_name api.example.com;
location /api/products { proxy_pass http://backend; add_header Cache-Control "public, max-age=60, stale-while-revalidate=300"; add_header Surrogate-Key "products"; add_header Surrogate-Key "product-$product_id" always; }
location /api/categories { proxy_pass http://backend; add_header Cache-Control "public, max-age=300"; add_header Surrogate-Key "categories"; }}Conditional purging headers:
location /api/data { proxy_pass http://backend;
# Enable conditional requests etag on; if_modified_since exact;
# Surrogate keys for targeted invalidation add_header Surrogate-Key "data data-$dataset_id"; add_header Cache-Control "public, max-age=300";}Apache Origin Server Configuration
Static files:
<VirtualHost *:80> ServerName static.example.com DocumentRoot /var/www/static
<Location "/assets/"> Header set Cache-Control "public, max-age=31536000, immutable" Header set Surrogate-Key "static-assets" </Location>
<Location "/images/"> Header set Cache-Control "public, max-age=2592000" </Location>
# Enable ETags FileETag MTime Size</VirtualHost>Dynamic content:
<VirtualHost *:80> ServerName api.example.com
ProxyPass /api/ http://backend:8080/api/ ProxyPassReverse /api/ http://backend:8080/api/
<Location "/api/products"> Header set Cache-Control "public, max-age=60, stale-while-revalidate=300" Header set Surrogate-Key "products" </Location></VirtualHost>CDN-Specific Configuration
Azion Rules Engine:
Rule 1: Static Assets Match: Path matches /static/* OR /assets/* Then: Cache TTL: 31536000 (1 year) Enable Cache: true Bypass Cache: false
Rule 2: API Endpoints Match: Path matches /api/* Then: Cache TTL: 60 Stale While Revalidate: 3600 Stale If Error: 86400
Rule 3: HTML Pages Match: Path matches /*.html OR Path matches / Then: Cache TTL: 300 Stale While Revalidate: 3600Cloudflare Page Rules:
Rule: static.example.com/assets/* Cache Level: Cache Everything Edge Cache TTL: 1 year Browser Cache TTL: 1 year
Rule: api.example.com/api/* Cache Level: Cache Everything Edge Cache TTL: 60 seconds Browser Cache TTL: 10 seconds Origin Cache Control: OnFastly VCL:
sub vcl_recv { # Static assets if (req.url ~ "^/assets/") { set req.http.Cache-Control = "public, max-age=31536000, immutable"; return (lookup); }
# API with surrogate keys if (req.url ~ "^/api/products") { set req.http.Surrogate-Key = "products"; return (lookup); }}
sub vcl_deliver { # Add surrogate key header for debugging if (resp.http.Surrogate-Key) { set resp.http.X-Surrogate-Key = resp.http.Surrogate-Key; }}Cache Invalidation
Invalidation Methods
| Method | Advantages | Disadvantages |
|---|---|---|
| TTL expiration | Simple, automatic | Stale until expired |
| Purge (manual/API) | Immediate control | Requires infrastructure |
| Soft purge | Updates in background | No immediate consistency guarantee |
| Webhook/Event-driven | Automated | Implementation complexity |
| Surrogate keys | Selective invalidation | Requires specific configuration |
Surrogate Keys Implementation Patterns
Surrogate keys enable selective invalidation without tracking every URL. Origin servers attach keys via headers, and CDNs index content by those keys.
Origin server header pattern:
HTTP/1.1 200 OKContent-Type: application/jsonCache-Control: public, max-age=3600Surrogate-Key: product-123 products category-electronicsKey naming conventions:
| Key Pattern | Purpose | Example |
|---|---|---|
{type}-{id} | Individual resource | product-456, user-789 |
{type} | All resources of type | products, categories |
{type}-{attr}-{value} | Filtered groups | product-category-electronics |
layout-{name} | Template dependencies | layout-homepage, layout-product |
dependency-{id} | Cross-resource deps | dependency-pricing, dependency-inventory |
Purge by surrogate key:
# Purge all productscurl -X POST https://api.cdn.example.com/purge \ -H "Content-Type: application/json" \ -d '{"surrogate_keys": ["products"]}'
# Purge specific product and its categorycurl -X POST https://api.cdn.example.com/purge \ -H "Content-Type: application/json" \ -d '{"surrogate_keys": ["product-123", "category-electronics"]}'
# Soft purge (mark stale, allow serving during revalidation)curl -X POST https://api.cdn.example.com/purge \ -H "Content-Type: application/json" \ -d '{"surrogate_keys": ["products"], "soft": true}'Webhook-Based Invalidation Architecture
┌──────────────┐ ┌──────────────┐ ┌──────────────┐│ CMS/DB │────▶│ Webhook │────▶│ Invalidation││ Update │ │ Handler │ │ Service │└──────────────┘ └──────────────┘ └──────┬───────┘ │ ┌─────────────────────────────┼─────────────────────────────┐ │ │ │ ▼ ▼ ▼ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ CDN API │ │ Message │ │ Audit │ │ (Purge) │ │ Queue │ │ Log │ └─────────────┘ └──────┬──────┘ └─────────────┘ │ ▼ ┌─────────────┐ │ Retry on │ │ Failure │ └─────────────┘Webhook handler example (Node.js):
const crypto = require('crypto');
async function handleWebhook(req, res) { const signature = req.headers['x-webhook-signature']; const payload = JSON.stringify(req.body);
const expectedSignature = crypto .createHmac('sha256', process.env.WEBHOOK_SECRET) .update(payload) .digest('hex');
if (signature !== expectedSignature) { return res.status(401).json({ error: 'Invalid signature' }); }
const { entity_type, entity_id, action } = req.body;
if (action === 'update' || action === 'delete') { const surrogateKey = `${entity_type}-${entity_id}`; await purgeCache([surrogateKey, entity_type]); }
res.status(200).json({ status: 'queued' });}
async function purgeCache(keys) { await fetch('https://api.cdn.example.com/purge', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.CDN_API_TOKEN}` }, body: JSON.stringify({ surrogate_keys: keys }) });}Event-driven invalidation with message queue:
const { Queue, Worker } = require('bullmq');
const purgeQueue = new Queue('cache-purge', { connection: { host: 'redis', port: 6379 }});
async function queuePurge(keys) { await purgeQueue.add('purge', { keys, timestamp: Date.now() });}
const worker = new Worker('cache-purge', async job => { const { keys } = job.data; const response = await fetch('https://api.cdn.example.com/purge', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.CDN_API_TOKEN}` }, body: JSON.stringify({ surrogate_keys: keys }) });
if (!response.ok) { throw new Error(`Purge failed: ${response.status}`); }}, { connection: { host: 'redis', port: 6379 }, attempts: 3, backoff: { type: 'exponential', delay: 1000 }});Cache Tag Systems
Cache tags extend surrogate keys with hierarchical relationships and inheritance.
Tag hierarchy example:
product-123 ├─ products (parent) ├─ category-electronics │ └─ categories (grandparent) └─ brand-sony └─ brands (grandparent)Purging by tag:
# Purge product-123 and all its cached variantscurl -X POST https://api.cdn.example.com/purge \ -d '{"tags": ["product-123"]}'
# Purge all productscurl -X POST https://api.cdn.example.com/purge \ -d '{"tags": ["products"]}'
# Purge by multiple tags (AND logic)curl -X POST https://api.cdn.example.com/purge \ -d '{"tags": ["products", "category-electronics"], "match": "all"}'Selective Invalidation vs Full Purge
| Scenario | Method | Reason |
|---|---|---|
| Single resource updated | Surrogate key purge | Precise, minimal origin load |
| Related resources updated | Multi-key purge | Efficient for small groups |
| Category/taxonomy changed | Tag-based purge | Catches all dependencies |
| Site-wide template change | Full purge | No way to enumerate affected URLs |
| Security incident | Full purge | Immediate removal required |
| Code deployment | Full purge | All cached responses may be stale |
| Pricing update | Soft purge + surrogate key | Allows graceful transition |
| Emergency content removal | Full purge | Speed over efficiency |
Full purge implementation:
# Immediate full purgecurl -X POST https://api.cdn.example.com/purge \ -H "Content-Type: application/json" \ -d '{"all": true}'
# Full purge by path prefixcurl -X POST https://api.cdn.example.com/purge \ -H "Content-Type: application/json" \ -d '{"prefixes": ["/api/", "/products/"]}'Rate Limiting for Purge Operations
CDN APIs typically rate-limit purge requests. Plan invalidation patterns accordingly.
| CDN | Purge Rate Limit | Purge Size Limit |
|---|---|---|
| Azion | 100 requests/minute | 100 URLs/request |
| Cloudflare | 1000 requests/minute | 500 URLs/request |
| Fastly | 100 requests/minute | 256 keys/request |
| AWS CloudFront | 3000 URLs/minute | 3000 URLs/request |
Batch invalidation strategy:
async function batchPurge(keys, batchSize = 100) { const batches = []; for (let i = 0; i < keys.length; i += batchSize) { batches.push(keys.slice(i, i + batchSize)); }
const results = []; for (const batch of batches) { const response = await purgeCache(batch); results.push(response); await sleep(600); // Rate limit buffer }
return results;}
function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms));}Performance Metrics
Cache Hit Ratio
Cache Hit Ratio = (Cache Hits / Total Requests) × 100
Example:- Cache Hits: 85,000- Cache Misses: 15,000- Total: 100,000- Hit Ratio: 85%Benchmarks by content type:
| Content Type | Expected Hit Ratio | Typical TTL |
|---|---|---|
| Static assets (CSS, JS, images) | 95-99% | 1 year (versioned) |
| Static pages (HTML) | 80-95% | 1-24 hours |
| Public APIs | 70-90% | 1-60 minutes |
| Authenticated APIs | 30-60% | 0-5 minutes |
| Streaming/Dynamic | 10-40% | 0-10 seconds |
Time to First Byte (TTFB)
| Scenario | Expected TTFB |
|---|---|
| Cache hit at edge | < 50ms |
| Cache miss (origin responding) | 100-500ms |
| Cache miss (slow origin) | 500-2000ms |
| Stale-while-revalidate | < 50ms (stale) + background revalidation |
Latency by Region
Typical latency with CDN vs without CDN:
| User Region | Without CDN | With CDN | Improvement |
|---|---|---|---|
| Same datacenter as origin | 20-50ms | 20-50ms | 0% |
| Same continent | 100-300ms | 20-80ms | 60-80% |
| Different continent | 200-800ms | 30-100ms | 75-90% |
Monitoring and Observability
Cache Hit Ratio Monitoring Setup
Prometheus metrics:
scrape_configs: - job_name: 'cdn-metrics' static_configs: - targets: ['cdn-exporter:9090'] metrics_path: /metricsKey metrics to track:
# Cache hit ratio (5-minute window)sum(rate(cdn_cache_hits_total[5m])) /sum(rate(cdn_requests_total[5m])) * 100
# Cache hit ratio by path patternsum by (path_pattern) (rate(cdn_cache_hits_total[5m])) /sum by (path_pattern) (rate(cdn_requests_total[5m])) * 100
# Miss rate spike detection(rate(cdn_cache_misses_total[5m]) - rate(cdn_cache_misses_total[5m] offset 1h)) / rate(cdn_cache_misses_total[5m] offset 1h) * 100TTFB Percentile Analysis
Percentile definitions:
| Percentile | Meaning | Action |
|---|---|---|
| p50 | 50% of requests faster than this | Baseline performance |
| p95 | 95% of requests faster than this | Identify slow patterns |
| p99 | 99% of requests faster than this | Worst-case user experience |
Prometheus percentile queries:
# TTFB percentileshistogram_quantile(0.50, sum(rate(cdn_ttfb_seconds_bucket[5m])) by (le))histogram_quantile(0.95, sum(rate(cdn_ttfb_seconds_bucket[5m])) by (le))histogram_quantile(0.99, sum(rate(cdn_ttfb_seconds_bucket[5m])) by (le))
# TTFB by cache statushistogram_quantile(0.95, sum by (le, cache_status) (rate(cdn_ttfb_seconds_bucket[5m])))Expected TTFB targets:
| Cache Status | p50 Target | p95 Target | p99 Target |
|---|---|---|---|
| Hit | < 30ms | < 50ms | < 100ms |
| Miss | < 200ms | < 500ms | < 1000ms |
| Stale | < 50ms | < 80ms | < 150ms |
Alerting Thresholds
Prometheus alerting rules:
groups: - name: cdn_cache_alerts rules: - alert: CacheHitRatioLow expr: | sum(rate(cdn_cache_hits_total[5m])) / sum(rate(cdn_requests_total[5m])) < 0.70 for: 5m labels: severity: warning annotations: summary: "Cache hit ratio below 70%" description: "Current ratio: {{ $value | humanizePercentage }}"
- alert: CacheHitRatioCritical expr: | sum(rate(cdn_cache_hits_total[5m])) / sum(rate(cdn_requests_total[5m])) < 0.50 for: 2m labels: severity: critical annotations: summary: "Cache hit ratio critically low" description: "Current ratio: {{ $value | humanizePercentage }}"
- alert: TTFBHighP95 expr: | histogram_quantile(0.95, sum(rate(cdn_ttfb_seconds_bucket[5m])) by (le)) > 0.5 for: 5m labels: severity: warning annotations: summary: "TTFB p95 above 500ms" description: "Current p95: {{ $value | humanizeDuration }}"
- alert: OriginErrorRate expr: | sum(rate(cdn_origin_errors_total[5m])) / sum(rate(cdn_origin_requests_total[5m])) > 0.05 for: 2m labels: severity: critical annotations: summary: "Origin error rate above 5%"Dashboard Examples
Grafana dashboard JSON (key panels):
{ "panels": [ { "title": "Cache Hit Ratio", "type": "stat", "targets": [{ "expr": "sum(rate(cdn_cache_hits_total[5m])) / sum(rate(cdn_requests_total[5m])) * 100" }], "thresholds": { "mode": "absolute", "steps": [ {"color": "red", "value": 0}, {"color": "yellow", "value": 70}, {"color": "green", "value": 90} ] } }, { "title": "TTFB Percentiles", "type": "graph", "targets": [ {"expr": "histogram_quantile(0.50, sum(rate(cdn_ttfb_seconds_bucket[5m])) by (le))", "legendFormat": "p50"}, {"expr": "histogram_quantile(0.95, sum(rate(cdn_ttfb_seconds_bucket[5m])) by (le))", "legendFormat": "p95"}, {"expr": "histogram_quantile(0.99, sum(rate(cdn_ttfb_seconds_bucket[5m])) by (le))", "legendFormat": "p99"} ] }, { "title": "Requests by Cache Status", "type": "piechart", "targets": [ {"expr": "sum(rate(cdn_cache_hits_total[5m]))", "legendFormat": "Hit"}, {"expr": "sum(rate(cdn_cache_misses_total[5m]))", "legendFormat": "Miss"}, {"expr": "sum(rate(cdn_cache_stale_total[5m]))", "legendFormat": "Stale"} ] } ]}Datadog dashboard queries:
# Cache hit ratiosum:cdn.cache.hits{*}.as_count() / sum:cdn.requests{*}.as_count() * 100
# TTFB percentilesquantile:p95:cdn.ttfb{*}quantile:p99:cdn.ttfb{*}
# Error rate by originsum:cdn.origin.errors{*} by {origin}.as_count() / sum:cdn.origin.requests{*} by {origin}.as_count() * 100Log Analysis for Cache Optimization
Key log fields to capture:
{ "timestamp": "2026-06-18T12:00:00Z", "request_id": "abc-123", "method": "GET", "path": "/api/products/456", "cache_status": "HIT", "ttfb_ms": 23, "origin_time_ms": null, "edge_location": "gru", "user_country": "BR", "response_size_bytes": 12345, "surrogate_keys": ["product-456", "products"], "cache_key": "/api/products/456"}Log queries for optimization:
-- Low hit ratio pathsSELECT path_pattern, COUNT(*) as total_requests, SUM(CASE WHEN cache_status = 'HIT' THEN 1 ELSE 0 END) as hits, SUM(CASE WHEN cache_status = 'HIT' THEN 1 ELSE 0 END) * 100.0 / COUNT(*) as hit_ratioFROM cdn_logsWHERE timestamp > NOW() - INTERVAL '1 day'GROUP BY path_patternHAVING COUNT(*) > 100ORDER BY hit_ratio ASCLIMIT 20;
-- High TTFB cache misses (origin problems)SELECT path, COUNT(*) as miss_count, AVG(origin_time_ms) as avg_origin_time, PERCENTILE(origin_time_ms, 95) as p95_origin_timeFROM cdn_logsWHERE cache_status = 'MISS' AND timestamp > NOW() - INTERVAL '1 hour'GROUP BY pathORDER BY p95_origin_time DESCLIMIT 20;
-- Unused surrogate keysSELECT surrogate_key, COUNT(*) as request_countFROM cdn_logsWHERE timestamp > NOW() - INTERVAL '7 days'GROUP BY surrogate_keyHAVING COUNT(*) < 10ORDER BY request_count ASC;Performance Optimization
Cache Warming Strategies
Cache warming pre-populates edge caches before user traffic arrives.
Proactive warming:
const warmCache = async (urls) => { const edgeLocations = ['gru', 'mia', 'fra', 'nrt', 'syd'];
for (const url of urls) { const warmPromises = edgeLocations.map(async (location) => { const warmUrl = `https://${location}.cdn.example.com${url}`; await fetch(warmUrl, { method: 'HEAD', headers: { 'X-Warm-Request': 'true' } }); });
await Promise.all(warmPromises); console.log(`Warmed: ${url}`); }};
// Run after deploymentsconst criticalPaths = [ '/', '/products', '/pricing', '/api/health'];
warmCache(criticalPaths);Scheduled warming for time-sensitive content:
const cron = require('node-cron');
// Warm product feeds every 15 minutescron.schedule('*/15 * * * *', async () => { const products = await getActiveProducts(); const urls = products.map(p => `/api/products/${p.id}`); await warmCache(urls);});
// Warm homepage before traffic peakscron.schedule('0 8 * * 1-5', async () => { await warmCache(['/', '/products', '/deals']);});Deployment-triggered warming:
- name: Warm CDN Cache run: | node scripts/warm-cache.js env: CDN_ENDPOINT: ${{ secrets.CDN_ENDPOINT }}Prefetching and Preloading
HTTP/2 Server Push (deprecated but still in use):
Link: </css/styles.css>; rel=preload; as=styleLink: </js/app.js>; rel=preload; as=scriptLink: </images/hero.webp>; rel=preload; as=imagePreload headers for critical resources:
location / { proxy_pass http://backend;
add_header Link "</css/critical.css>; rel=preload; as=style"; add_header Link "</js/header.js>; rel=preload; as=script"; add_header Link "https://fonts.example.com/font.woff2>; rel=preload; as=font; crossorigin";}Browser prefetch hints:
<link rel="prefetch" href="/api/products" as="fetch" crossorigin><link rel="preconnect" href="https://cdn.example.com"><link rel="dns-prefetch" href="https://cdn.example.com">Edge-initiated prefetch:
// Edge function for predictive prefetchaddEventListener('fetch', event => { event.respondWith(handleRequest(event));});
async function handleRequest(event) { const response = await fetch(event.request);
// Add prefetch headers for likely next requests if (event.request.url.includes('/products')) { response.headers.set('Link', '</api/cart>; rel=prefetch; as=fetch, ' + '</api/recommendations>; rel=prefetch; as=fetch' ); }
return response;}Compression at Edge
Brotli configuration:
# Origin serverbrotli on;brotli_comp_level 4;brotli_types text/plain text/css application/json application/javascript image/svg+xml;brotli_min_length 256;CDN-level compression settings:
Azion Rules Engine: Match: Content-Type matches text/* OR application/json OR application/javascript Then: Compression: Brotli (quality 4) Minimum Size: 256 bytes MIME Types: text/html, text/css, application/javascript, application/json, image/svg+xmlCompression comparison:
| Algorithm | Compression Ratio | CPU Cost | Browser Support |
|---|---|---|---|
| gzip (level 6) | 70-75% | Medium | Universal |
| Brotli (level 4) | 75-82% | Higher | 97%+ |
| Brotli (level 11) | 80-85% | Very High | 97%+ |
Best practice:
- Use Brotli level 4 for dynamic content (balance of compression and CPU)
- Use Brotli level 11 for static assets during build time
- Fall back to gzip for older clients
HTTP/2 and HTTP/3 Considerations
HTTP/2 benefits for cached content:
| Feature | Benefit | Impact |
|---|---|---|
| Multiplexing | Multiple requests over one connection | Reduces connection overhead |
| Header compression (HPACK) | Smaller headers | 85% header size reduction |
| Server Push (deprecated) | Preemptive resource delivery | Use rel=preload instead |
HTTP/3 (QUIC) benefits:
| Feature | Benefit | Impact |
|---|---|---|
| 0-RTT connection | Resume connections instantly | Faster repeat visits |
| UDP-based | No TCP head-of-line blocking | Better on lossy networks |
| Improved multiplexing | Independent streams | No stream interference |
CDN configuration for HTTP/3:
Azion Edge Application Settings: HTTP/3: Enabled QUIC: Enabled 0-RTT: Enabled for safe methods (GET, HEAD)Cache considerations for HTTP/3:
// 0-RTT requests may arrive before cache invalidation propagates// Mark cache responses with version or use soft purgesresponse.headers.set('Cache-Version', buildVersion);Image Optimization at CDN Level
Automatic image optimization:
CDN Rules: Match: Path matches /images/* AND Accept includes image/webp Then: Transform: WebP (quality 85)
Match: Path matches /images/* AND Width header exists Then: Resize: {width}x auto Format: WebPImage cache strategy:
Original image: /images/photo.jpg Cache TTL: 1 year (immutable) Surrogate-Key: image-photo123
Optimized variants: /images/photo.jpg?w=800&format=webp Cache TTL: 1 year (immutable) Surrogate-Key: image-photo123 image-photo123-variantResponsive images with CDN:
<picture> <source media="(min-width: 1200px)" srcset="https://cdn.example.com/images/hero.jpg?w=1920&format=webp"> <source media="(min-width: 768px)" srcset="https://cdn.example.com/images/hero.jpg?w=1200&format=webp"> <img src="https://cdn.example.com/images/hero.jpg?w=800&format=webp" loading="lazy"></picture>Cache Troubleshooting
Common Cache Issues Diagnosis
| Symptom | Likely Cause | Diagnosis Step |
|---|---|---|
| Stale content served | TTL too long or purge failed | Check Cache-Control headers and purge logs |
| Cache hit ratio low | Short TTL or unique URLs | Analyze cache keys and query strings |
| Origin overload | Cache bypass or misses | Check for no-cache headers or cookies |
| 502 errors on purge | Rate limited or invalid keys | Review purge API logs |
| Inconsistent content | Multiple cache keys for same URL | Check Vary header and query strings |
| Content not caching | private or no-store | Inspect response headers |
| High TTFB on hits | Large response size or slow edge | Check response size and edge location |
Cache Debugging Tools and Techniques
curl with verbose headers:
# Check cache statuscurl -I -s https://example.com/api/products | grep -i "x-cache\|age\|cache-control"
# Example outputx-cache: HITage: 2345cache-control: public, max-age=3600Cache debugging headers:
# Force cache miss (if supported)curl -H "Cache-Control: no-cache" https://example.com/api/data
# Check specific edge locationcurl -H "X-Edge-Location: debug" https://example.com/api/data
# Add debug headercurl -H "X-Cache-Debug: true" https://example.com/api/dataOnline tools:
| Tool | Purpose | URL |
|---|---|---|
| WebPageTest | Full cache analysis | webpagetest.org |
| Chrome DevTools | Cache headers and timing | Built-in |
| KeyCDN Cache Check | HTTP header analysis | keycdn.com/tools/cdn-check |
| Redbot | Cache-Control validation | redbot.org |
Header Analysis Workflow
Step 1: Check response headers:
curl -I https://example.com/api/products
# Look for:# - Cache-Control: public, max-age=?# - Age: ? (how long cached)# - X-Cache: HIT/MISS# - Surrogate-Key: ?# - Vary: ? (what creates different cache keys)Step 2: Verify cache key:
# Check if query string affects cachingcurl -I "https://example.com/api/products?v=1"curl -I "https://example.com/api/products?v=2"
# Same response = query string ignored# Different cache status = query string in cache keyStep 3: Test invalidation:
# Purge and verifycurl -X PURGE https://api.cdn.example.com/purge -d '{"url": "/api/products"}'
# Immediate re-fetch should be MISScurl -I https://example.com/api/products | grep "X-Cache"# Expected: X-Cache: MISSStep 4: Analyze timing:
# Measure TTFBcurl -w "TTFB: %{time_starttransfer}s\n" -o /dev/null -s https://example.com/api/products
# Compare cache hit vs miss# Hit should be < 50ms# Miss should show origin latencyCache Bypass Methods
Method 1: URL modification:
# Add unique query stringcurl https://example.com/api/products?nocache=12345
# Note: Only works if CDN doesn't normalize query stringsMethod 2: Header-based bypass:
# Authorization header (most CDNs bypass cache)curl -H "Authorization: Bearer test" https://example.com/api/products
# Custom bypass headercurl -H "X-Cache-Bypass: true" https://example.com/api/productsMethod 3: Cookie bypass:
# Many CDNs bypass cache when cookies presentcurl -H "Cookie: session=abc123" https://example.com/api/productsMethod 4: HTTP method:
# POST requests typically bypass cachecurl -X POST https://example.com/api/products -d '{"test": true}'CDN-specific bypass:
# Azioncurl -H "X-Debug: true" https://example.com/api/products
# Cloudflarecurl -H "CF-Cache-Status: BYPASS" https://example.com/api/products
# Fastlycurl -H "Fastly-Debug: 1" https://example.com/api/productsSigns of Cache Problems
- Cache hit ratio below 70% for static content
- TTFB consistently above 200ms for cached pages
- Traffic spikes at origin during events
- Users reporting stale content
- High origin egress costs
- 502/504 errors during mass invalidations
Common Mistakes and Fixes
Mistake: TTL too short for versioned assets Fix: Assets with hash in name can have 1 year TTL (max-age=31536000)
Mistake: Purge in infinite loop (invalidate → revalidate → invalidate) Fix: Implement debounce or cooldown between invalidations
Mistake: Conflicting Cache-Control (max-age + no-cache) Fix: Use consistent directives, no-cache requires revalidation always
Mistake: Ignoring query strings in cache Fix: Configure CDN to normalize or ignore query strings when appropriate
Mistake: Not configuring stale handlers Fix: Always configure stale-if-error for resilience
Mistake: Over-purging during deployments Fix: Use surrogate keys for selective invalidation instead of full purges
Mistake: Not monitoring cache hit ratio Fix: Set up alerts for hit ratio drops below thresholds
Mistake: Ignoring Vary header implications Fix: Audit Vary headers to prevent cache key explosion
Use Cases
E-commerce with Dynamic Inventory
Product pages: TTL 5 minutes + stale-while-revalidate 1 hour. Inventory API: TTL 30 seconds. Manual purge when updating price or stock.
# Product pageCache-Control: public, max-age=300, stale-while-revalidate=3600, stale-if-error=86400Surrogate-Key: product-123 products category-electronics
# Inventory APICache-Control: public, max-age=30, stale-if-error=300Surrogate-Key: inventory product-123-inventoryNews Portal
Articles: TTL 1 hour + stale-while-revalidate 24 hours. Breaking news: immediate purge via webhook. Homepage: TTL 5 minutes + purge on publish.
// Webhook handler for article publicationasync function onArticlePublish(article) { const keys = [ `article-${article.id}`, 'homepage', `category-${article.category}` ]; await purgeCache(keys);}SaaS Dashboard
Public data (plans, features): TTL 24 hours. User dashboard: private, no-store. Metrics API: TTL 1 minute + stale-if-error 5 minutes.
# Public pricing pageCache-Control: public, max-age=86400, stale-while-revalidate=604800Surrogate-Key: pricing plans
# User dashboard (no cache)Cache-Control: private, no-store
# Metrics APICache-Control: public, max-age=60, stale-if-error=300, s-maxage=60Surrogate-Key: metricsVideo Streaming
Video segments: TTL 1 year (immutable). Playlist/manifest: TTL 10 seconds. Geo-blocking: evaluated at edge, no cache.
# Video segmentCache-Control: public, max-age=31536000, immutableSurrogate-Key: video-abc123
# HLS playlistCache-Control: public, max-age=10Surrogate-Key: playlist-abc123
# Geo-blocked content (evaluated per request)Cache-Control: no-storeFrequently Asked Questions
What is the difference between TTL and cache invalidation? TTL (Time-to-Live) defines how long content stays in cache before expiring automatically. Invalidation removes or updates content before TTL expires, ensuring immediate consistency.
When should I use stale-while-revalidate? Use when high availability is more important than immediate consistency. Users get stale content instantly while CDN fetches the updated version in background.
How to calculate ideal cache hit ratio? Monitor hit ratio by content type. Static assets should be 95%+. Dynamic content varies. If hit ratio drops, check TTL, access patterns, and query string configuration.
What is surrogate key and when to use it? Surrogate key is an additional identifier for content groups. Allows invalidating multiple URLs with a single key. Use when related content needs to be updated together (e.g., all posts in a category).
Can private cache be stored in CDN? Not by default. Cache-Control: private indicates only browser can cache. CDNs respect this directive. For shared cache with access control, use public with authentication at edge.
How to avoid accidental full cache purge? Implement protections: require confirmation for broad purges, limit purge frequency, use surrogate keys for selective invalidation, monitor and alert on suspicious purges.
What is the difference between PURGE and BAN? PURGE removes content from cache immediately. BAN marks content as invalid but may serve stale until revalidation. PURGE is more aggressive, BAN is more gradual.
How to handle cache for authenticated APIs? Use Cache-Control: private or no-store for user-specific data. For data that can be cached per user, use Vary: Authorization headers or cache keys that include user ID.
Is CDN cache the same as browser cache? No. CDN cache stores on edge server, serving multiple users. Browser cache is local on user’s device. CDNs use s-maxage for their cache, browsers use max-age.
How to configure cache for Single Page Applications? HTML (index.html): no-cache or short TTL (always revalidates). Versioned JS/CSS: long TTL (1 year). APIs: TTL by criticality. SPA needs to revalidate HTML to get new chunks.
What TTL should I use for API responses? Public APIs: 1-60 minutes depending on data freshness requirements. Authenticated APIs: private, no-store for user-specific data, or per-user cache with user ID in cache key.
How do surrogate keys differ from cache tags? Surrogate keys are flat identifiers attached to content. Cache tags add hierarchical relationships, allowing parent tag purges to affect child content.
When should I use cache warming? Use cache warming after deployments, before marketing campaigns, for time-sensitive content, and when origin latency significantly impacts user experience.
How to handle cache for A/B testing? Use Vary: X-Experiment-Id header or include experiment ID in URL. Each variant gets a separate cache entry.
What causes cache hit ratio to drop suddenly? New URL patterns not cached, query string explosion, Vary header changes, origin errors causing cache bypass, or traffic pattern changes.
How to monitor cache performance across multiple regions? Deploy monitoring at each edge location, aggregate metrics by region, compare hit ratios and TTFB across regions, set region-specific alerts.
What is the impact of cookies on CDN cache? Many CDNs bypass cache when cookies are present. For public content, strip cookies at edge or mark responses as cacheable despite cookies.
How to test cache configuration before deployment? Use staging environments, test with debug headers, validate TTL and stale handlers, simulate purge operations, measure TTFB impact.
What is cache key normalization? Normalizing cache keys by ignoring certain query parameters, headers, or URL variations to increase cache hits. Example: ignoring utm_* parameters.
How to handle cache for GraphQL APIs? Cache by query hash or operation name. Include variables in cache key if they affect response. Use persisted queries for stable cache keys.
How This Applies in Practice
Well-configured CDN cache reduces latency by 60-90% and origin load by 70-95%. The cost of invalidation (purges, webhooks) is offset by bandwidth savings and better user experience.
Configure aggressive TTLs for immutable content, stale handlers for resilience, and selective invalidation for changing content. Monitor hit ratio and TTFB continuously. Adjust TTLs based on real access patterns, not assumptions.
How to Implement with Azion
Azion offers complete cache control at the edge:
- Edge Cache: Configure TTLs, stale handlers, and path-based rules
- Edge Functions: Implement custom cache logic in JavaScript/Wasm
- Cache API: Program invalidations via GraphQL or REST API
- Real-Time Metrics: Monitor hit ratio, TTFB, and volume by path
Typical Azion configuration:
Rules Engine:- If: Path matches /static/* Then: Cache TTL = 31536000 (1 year)
- If: Path matches /api/* Then: Cache TTL = 60, stale-while-revalidate = 3600
- If: Path matches /page/* Then: Cache TTL = 300, stale-if-error = 86400Cache warming with Azion Edge Functions:
addEventListener('scheduled', event => { const criticalPaths = ['/', '/products', '/pricing']; for (const path of criticalPaths) { fetch(`https://example.com${path}`, { method: 'HEAD' }); }});Invalidation via Azion API:
curl -X POST https://api.azion.com/v3/cache/purge \ -H "Authorization: Token $AZION_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "urls": ["https://example.com/api/products"], "layer": "edge" }'Learn more in the Azion Serverless Applications documentation.
Related Resources
Sources:
- RFC 7234. “Hypertext Transfer Protocol (HTTP/1.1): Caching.” IETF. 2014.
- HTTP Archive. “Web Almanac 2024: Caching.” 2024.
- Google. “HTTP/3: From root to tip.” 2024.
- Brotli. “Compression Algorithm Specification.” 2024.