CDN Cache, Invalidation and Performance: Technical Implementation

Technical implementation guide for CDN cache configuration, invalidation patterns, monitoring, and performance optimization. Includes TTL recommendations, surrogate keys, cache warming, and troubleshooting workflows.

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

StrategyHow It WorksWhen to Use
TTL (Time-to-Live)Content expires after defined timeStatic content, low change frequency
Stale-While-RevalidateServes stale, updates in backgroundHigh availability, stale tolerance
Stale-If-ErrorServes stale if origin failsResilience, fallback
Cache-AsideApplication manages cache explicitlyDynamic APIs, full control
Write-ThroughUpdates cache on writeCritical data, immediate consistency

HTTP Cache Control Headers

Cache-Control

DirectiveMeaningExample Use
max-age=3600Cache valid for 3600 secondsVersioned assets
s-maxage=3600TTL for CDN (different from browser)CDN-specific
publicCan be cached by anyonePublic content
privateBrowser cache onlyUser data
no-cacheRevalidate before servingSensitive content
no-storeDo not cacheSensitive data
stale-while-revalidate=86400Serve stale for 24h while revalidatingHigh availability
stale-if-error=3600Serve stale if origin returns errorResilience

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=3600

Cache Configuration by Content Type

TTL Recommendations by Content Type

Content TypeRecommended TTLCache-Control Example
Versioned static assets (JS, CSS, images)1 year (immutable)public, max-age=31536000, immutable
Unversioned images30 dayspublic, max-age=2592000
HTML pages (static)1-24 hourspublic, max-age=3600, stale-while-revalidate=86400
HTML pages (dynamic)0-5 minutespublic, max-age=300, stale-while-revalidate=3600
API responses (public)1-60 minutespublic, max-age=60, s-maxage=300
API responses (authenticated)No cache or per-userprivate, no-cache
Video/audio segments1 year (immutable)public, max-age=31536000, immutable
Video manifests (HLS/DASH)1-10 secondspublic, max-age=10
Fonts1 yearpublic, max-age=31536000, immutable
JSON data (rarely changes)1 hourpublic, max-age=3600, stale-if-error=86400

Cache-Control Header Examples

Static assets with content hashing:

Cache-Control: public, max-age=31536000, immutable

Content 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=86400

HTML 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=300

Browser 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: 3600

Cloudflare 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: On

Fastly 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

MethodAdvantagesDisadvantages
TTL expirationSimple, automaticStale until expired
Purge (manual/API)Immediate controlRequires infrastructure
Soft purgeUpdates in backgroundNo immediate consistency guarantee
Webhook/Event-drivenAutomatedImplementation complexity
Surrogate keysSelective invalidationRequires 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 OK
Content-Type: application/json
Cache-Control: public, max-age=3600
Surrogate-Key: product-123 products category-electronics

Key naming conventions:

Key PatternPurposeExample
{type}-{id}Individual resourceproduct-456, user-789
{type}All resources of typeproducts, categories
{type}-{attr}-{value}Filtered groupsproduct-category-electronics
layout-{name}Template dependencieslayout-homepage, layout-product
dependency-{id}Cross-resource depsdependency-pricing, dependency-inventory

Purge by surrogate key:

Terminal window
# Purge all products
curl -X POST https://api.cdn.example.com/purge \
-H "Content-Type: application/json" \
-d '{"surrogate_keys": ["products"]}'
# Purge specific product and its category
curl -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:

Terminal window
# Purge product-123 and all its cached variants
curl -X POST https://api.cdn.example.com/purge \
-d '{"tags": ["product-123"]}'
# Purge all products
curl -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

ScenarioMethodReason
Single resource updatedSurrogate key purgePrecise, minimal origin load
Related resources updatedMulti-key purgeEfficient for small groups
Category/taxonomy changedTag-based purgeCatches all dependencies
Site-wide template changeFull purgeNo way to enumerate affected URLs
Security incidentFull purgeImmediate removal required
Code deploymentFull purgeAll cached responses may be stale
Pricing updateSoft purge + surrogate keyAllows graceful transition
Emergency content removalFull purgeSpeed over efficiency

Full purge implementation:

Terminal window
# Immediate full purge
curl -X POST https://api.cdn.example.com/purge \
-H "Content-Type: application/json" \
-d '{"all": true}'
# Full purge by path prefix
curl -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.

CDNPurge Rate LimitPurge Size Limit
Azion100 requests/minute100 URLs/request
Cloudflare1000 requests/minute500 URLs/request
Fastly100 requests/minute256 keys/request
AWS CloudFront3000 URLs/minute3000 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 TypeExpected Hit RatioTypical TTL
Static assets (CSS, JS, images)95-99%1 year (versioned)
Static pages (HTML)80-95%1-24 hours
Public APIs70-90%1-60 minutes
Authenticated APIs30-60%0-5 minutes
Streaming/Dynamic10-40%0-10 seconds

Time to First Byte (TTFB)

ScenarioExpected 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 RegionWithout CDNWith CDNImprovement
Same datacenter as origin20-50ms20-50ms0%
Same continent100-300ms20-80ms60-80%
Different continent200-800ms30-100ms75-90%

Monitoring and Observability

Cache Hit Ratio Monitoring Setup

Prometheus metrics:

prometheus.yml
scrape_configs:
- job_name: 'cdn-metrics'
static_configs:
- targets: ['cdn-exporter:9090']
metrics_path: /metrics

Key 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 pattern
sum 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)
* 100

TTFB Percentile Analysis

Percentile definitions:

PercentileMeaningAction
p5050% of requests faster than thisBaseline performance
p9595% of requests faster than thisIdentify slow patterns
p9999% of requests faster than thisWorst-case user experience

Prometheus percentile queries:

# TTFB percentiles
histogram_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 status
histogram_quantile(0.95, sum by (le, cache_status)
(rate(cdn_ttfb_seconds_bucket[5m])))

Expected TTFB targets:

Cache Statusp50 Targetp95 Targetp99 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 ratio
sum:cdn.cache.hits{*}.as_count() / sum:cdn.requests{*}.as_count() * 100
# TTFB percentiles
quantile:p95:cdn.ttfb{*}
quantile:p99:cdn.ttfb{*}
# Error rate by origin
sum:cdn.origin.errors{*} by {origin}.as_count()
/ sum:cdn.origin.requests{*} by {origin}.as_count() * 100

Log 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 paths
SELECT
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_ratio
FROM cdn_logs
WHERE timestamp > NOW() - INTERVAL '1 day'
GROUP BY path_pattern
HAVING COUNT(*) > 100
ORDER BY hit_ratio ASC
LIMIT 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_time
FROM cdn_logs
WHERE cache_status = 'MISS'
AND timestamp > NOW() - INTERVAL '1 hour'
GROUP BY path
ORDER BY p95_origin_time DESC
LIMIT 20;
-- Unused surrogate keys
SELECT
surrogate_key,
COUNT(*) as request_count
FROM cdn_logs
WHERE timestamp > NOW() - INTERVAL '7 days'
GROUP BY surrogate_key
HAVING COUNT(*) < 10
ORDER 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 deployments
const criticalPaths = [
'/',
'/products',
'/pricing',
'/api/health'
];
warmCache(criticalPaths);

Scheduled warming for time-sensitive content:

const cron = require('node-cron');
// Warm product feeds every 15 minutes
cron.schedule('*/15 * * * *', async () => {
const products = await getActiveProducts();
const urls = products.map(p => `/api/products/${p.id}`);
await warmCache(urls);
});
// Warm homepage before traffic peaks
cron.schedule('0 8 * * 1-5', async () => {
await warmCache(['/', '/products', '/deals']);
});

Deployment-triggered warming:

.github/workflows/deploy.yml
- 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=style
Link: </js/app.js>; rel=preload; as=script
Link: </images/hero.webp>; rel=preload; as=image

Preload 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 prefetch
addEventListener('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 server
brotli 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+xml

Compression comparison:

AlgorithmCompression RatioCPU CostBrowser Support
gzip (level 6)70-75%MediumUniversal
Brotli (level 4)75-82%Higher97%+
Brotli (level 11)80-85%Very High97%+

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:

FeatureBenefitImpact
MultiplexingMultiple requests over one connectionReduces connection overhead
Header compression (HPACK)Smaller headers85% header size reduction
Server Push (deprecated)Preemptive resource deliveryUse rel=preload instead

HTTP/3 (QUIC) benefits:

FeatureBenefitImpact
0-RTT connectionResume connections instantlyFaster repeat visits
UDP-basedNo TCP head-of-line blockingBetter on lossy networks
Improved multiplexingIndependent streamsNo 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 purges
response.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: WebP

Image 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-variant

Responsive 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

SymptomLikely CauseDiagnosis Step
Stale content servedTTL too long or purge failedCheck Cache-Control headers and purge logs
Cache hit ratio lowShort TTL or unique URLsAnalyze cache keys and query strings
Origin overloadCache bypass or missesCheck for no-cache headers or cookies
502 errors on purgeRate limited or invalid keysReview purge API logs
Inconsistent contentMultiple cache keys for same URLCheck Vary header and query strings
Content not cachingprivate or no-storeInspect response headers
High TTFB on hitsLarge response size or slow edgeCheck response size and edge location

Cache Debugging Tools and Techniques

curl with verbose headers:

Terminal window
# Check cache status
curl -I -s https://example.com/api/products | grep -i "x-cache\|age\|cache-control"
# Example output
x-cache: HIT
age: 2345
cache-control: public, max-age=3600

Cache debugging headers:

Terminal window
# Force cache miss (if supported)
curl -H "Cache-Control: no-cache" https://example.com/api/data
# Check specific edge location
curl -H "X-Edge-Location: debug" https://example.com/api/data
# Add debug header
curl -H "X-Cache-Debug: true" https://example.com/api/data

Online tools:

ToolPurposeURL
WebPageTestFull cache analysiswebpagetest.org
Chrome DevToolsCache headers and timingBuilt-in
KeyCDN Cache CheckHTTP header analysiskeycdn.com/tools/cdn-check
RedbotCache-Control validationredbot.org

Header Analysis Workflow

Step 1: Check response headers:

Terminal window
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:

Terminal window
# Check if query string affects caching
curl -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 key

Step 3: Test invalidation:

Terminal window
# Purge and verify
curl -X PURGE https://api.cdn.example.com/purge -d '{"url": "/api/products"}'
# Immediate re-fetch should be MISS
curl -I https://example.com/api/products | grep "X-Cache"
# Expected: X-Cache: MISS

Step 4: Analyze timing:

Terminal window
# Measure TTFB
curl -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 latency

Cache Bypass Methods

Method 1: URL modification:

Terminal window
# Add unique query string
curl https://example.com/api/products?nocache=12345
# Note: Only works if CDN doesn't normalize query strings

Method 2: Header-based bypass:

Terminal window
# Authorization header (most CDNs bypass cache)
curl -H "Authorization: Bearer test" https://example.com/api/products
# Custom bypass header
curl -H "X-Cache-Bypass: true" https://example.com/api/products

Method 3: Cookie bypass:

Terminal window
# Many CDNs bypass cache when cookies present
curl -H "Cookie: session=abc123" https://example.com/api/products

Method 4: HTTP method:

Terminal window
# POST requests typically bypass cache
curl -X POST https://example.com/api/products -d '{"test": true}'

CDN-specific bypass:

# Azion
curl -H "X-Debug: true" https://example.com/api/products
# Cloudflare
curl -H "CF-Cache-Status: BYPASS" https://example.com/api/products
# Fastly
curl -H "Fastly-Debug: 1" https://example.com/api/products

Signs 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 page
Cache-Control: public, max-age=300, stale-while-revalidate=3600, stale-if-error=86400
Surrogate-Key: product-123 products category-electronics
# Inventory API
Cache-Control: public, max-age=30, stale-if-error=300
Surrogate-Key: inventory product-123-inventory

News 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 publication
async 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 page
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
Surrogate-Key: pricing plans
# User dashboard (no cache)
Cache-Control: private, no-store
# Metrics API
Cache-Control: public, max-age=60, stale-if-error=300, s-maxage=60
Surrogate-Key: metrics

Video Streaming

Video segments: TTL 1 year (immutable). Playlist/manifest: TTL 10 seconds. Geo-blocking: evaluated at edge, no cache.

# Video segment
Cache-Control: public, max-age=31536000, immutable
Surrogate-Key: video-abc123
# HLS playlist
Cache-Control: public, max-age=10
Surrogate-Key: playlist-abc123
# Geo-blocked content (evaluated per request)
Cache-Control: no-store

Frequently 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:

  1. Edge Cache: Configure TTLs, stale handlers, and path-based rules
  2. Edge Functions: Implement custom cache logic in JavaScript/Wasm
  3. Cache API: Program invalidations via GraphQL or REST API
  4. 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 = 86400

Cache 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:

Terminal window
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.


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.
stay up to date

Subscribe to our Newsletter

Get the latest product updates, event highlights, and tech industry insights delivered to your inbox.