CDN Caching: Accelerating Content Delivery at the Edge
A Content Delivery Network (CDN) is a globally distributed network of servers that caches content close to end users. When a user in Tokyo requests your website hosted in Virginia, instead of traversing the Pacific Ocean for every request, the CDN serves cached content from a server in Tokyo — reducing latency from hundreds of milliseconds to single-digit milliseconds. CDN caching is the outermost layer of your caching strategy and one of the most impactful performance optimizations for any web application.
How CDN Caching Works
CDNs operate on a pull model. When a user requests content from an edge server that does not have it cached (a cache miss), the edge server fetches it from your origin server, caches it, and serves it to the user. Subsequent requests for the same content from nearby users are served directly from the edge — no trip to the origin required.
The caching behavior is controlled primarily through HTTP cache headers that your origin server sends with each response.
Cache-Control Headers
The Cache-Control header is the primary mechanism for controlling how CDNs (and browsers) cache your content.
# Cache for 1 hour on CDN and browser
Cache-Control: public, max-age=3600
# Cache on CDN for 1 day, browser for 5 minutes
Cache-Control: public, max-age=300, s-maxage=86400
# Do not cache at all
Cache-Control: no-store
# Cache but always revalidate before serving
Cache-Control: no-cache
# Cache on CDN but not in browser (for personalized CDN content)
Cache-Control: s-maxage=3600, private
# Stale while revalidating (serve stale, fetch fresh in background)
Cache-Control: public, max-age=3600, stale-while-revalidate=60
Key Directives
| Directive | Meaning | Use Case |
|---|---|---|
public |
Any cache (CDN, browser, proxy) can store this | Static assets, public API responses |
private |
Only browser can cache; CDN must not | User-specific content (dashboard, profile) |
max-age=N |
Cache is valid for N seconds | All cacheable content |
s-maxage=N |
Override max-age for shared caches (CDN) | Different CDN vs browser TTL |
no-cache |
Cache it, but revalidate on every request | Frequently changing content |
no-store |
Do not cache at all | Sensitive data (banking, health) |
stale-while-revalidate=N |
Serve stale for N seconds while fetching fresh | High-traffic content that tolerates brief staleness |
immutable |
Content will never change (skip revalidation) | Versioned static assets (app.a1b2c3.js) |
ETag and Conditional Requests
ETags enable efficient revalidation. When cached content expires, the CDN sends a conditional request with If-None-Match. If the content has not changed, the origin responds with 304 Not Modified (no body), saving bandwidth.
# Origin response (first request)
HTTP/1.1 200 OK
ETag: "a1b2c3d4e5"
Cache-Control: public, max-age=3600
Content-Type: application/json
{"products": [...]}
# CDN revalidation request (after TTL expires)
GET /api/products HTTP/1.1
If-None-Match: "a1b2c3d4e5"
# Origin response (content unchanged)
HTTP/1.1 304 Not Modified
ETag: "a1b2c3d4e5"
Configuring CDN Caching
Nginx Origin Configuration
# Nginx configuration for CDN-friendly caching
# Static assets — cache aggressively with versioned filenames
location ~* \.(js|css|png|jpg|woff2)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# API responses — short cache, CDN can cache longer
location /api/ {
add_header Cache-Control "public, max-age=60, s-maxage=300";
add_header Vary "Accept-Encoding, Authorization";
}
# HTML pages — revalidate every time
location / {
add_header Cache-Control "public, no-cache";
add_header ETag $request_uri;
}
# User-specific content — do not cache on CDN
location /dashboard/ {
add_header Cache-Control "private, no-store";
}
Vary Header
The Vary header tells CDNs that different versions of the same URL exist based on request headers. This is critical for serving different content based on encoding, language, or device type.
# Different cached versions based on encoding
Vary: Accept-Encoding
# Different cached versions based on language
Vary: Accept-Language
# Multiple variation dimensions
Vary: Accept-Encoding, Accept-Language, User-Agent
Cache Purging and Invalidation
When content changes, you need to remove stale copies from CDN edge servers. This is the CDN equivalent of cache invalidation.
Purge Methods
- URL purge: Remove a specific URL from all edge servers. Fast but does not scale for bulk changes.
- Wildcard purge: Remove all URLs matching a pattern (e.g.,
/api/products/*). Useful for invalidating entire sections. - Tag-based purge (Surrogate Keys): Tag cached responses and purge by tag. For example, tag all product responses with
product-catalogand purge that tag when the catalog changes. - Full purge: Clear the entire CDN cache. Nuclear option — causes a flood of origin requests.
# Cloudflare API purge example
import requests
def purge_cdn_urls(urls):
response = requests.post(
f"https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/purge_cache",
headers={
"Authorization": f"Bearer {API_TOKEN}",
"Content-Type": "application/json"
},
json={"files": urls}
)
return response.json()
# Purge specific URLs
purge_cdn_urls([
"https://example.com/api/products/1001",
"https://example.com/api/products/1002"
])
# Purge everything (use with caution)
def purge_all():
requests.post(
f"https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/purge_cache",
headers={"Authorization": f"Bearer {API_TOKEN}"},
json={"purge_everything": True}
)
Cache Key Design
The cache key determines what makes a request "unique" from the CDN's perspective. By default, the cache key is the full URL including query parameters.
Optimizing cache keys can dramatically improve hit ratios:
- Ignore irrelevant query parameters: Tracking parameters like
utm_sourceshould not create separate cache entries. - Sort query parameters:
?a=1&b=2and?b=2&a=1should hit the same cache entry. - Include relevant headers: If you serve different content based on
Accept-Language, include it in the cache key.
Edge Computing
Modern CDNs go beyond simple caching — they allow you to run code at the edge. Platforms like Cloudflare Workers, AWS Lambda@Edge, and Vercel Edge Functions let you execute logic at CDN edge servers, combining the low latency of CDN caching with dynamic content generation.
// Cloudflare Worker example — personalized caching at the edge
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const country = request.headers.get('CF-IPCountry')
const cacheKey = new Request(request.url + '?country=' + country)
const cache = caches.default
let response = await cache.match(cacheKey)
if (!response) {
response = await fetch(request)
const headers = new Headers(response.headers)
headers.set('Cache-Control', 'public, max-age=300')
response = new Response(response.body, { headers })
event.waitUntil(cache.put(cacheKey, response.clone()))
}
return response
}
CDN Provider Comparison
| Feature | Cloudflare | AWS CloudFront | Akamai |
|---|---|---|---|
| Edge Locations | 300+ cities | 400+ edge locations | 4,000+ locations |
| Edge Computing | Workers (V8 isolates) | Lambda@Edge / Functions | EdgeWorkers |
| Free Tier | Generous free plan | 1 TB/month (12 months) | Enterprise only |
| Purge Speed | ~2 seconds global | Seconds to minutes | Seconds |
| Best For | General purpose, DDoS protection | AWS ecosystem integration | Enterprise, media streaming |
Best Practices
- Version your static assets: Use content-hash filenames (e.g.,
app.a1b2c3.js) with long TTLs andimmutable. When content changes, the filename changes, guaranteeing fresh content. - Use
stale-while-revalidate: This serves slightly stale content while fetching fresh data in the background, eliminating perceived latency. It is especially useful for high-traffic pages. - Never cache content with
Set-Cookieheaders: Cookies are user-specific; caching them at the CDN serves one user's cookies to everyone. - Monitor cache hit ratios: A healthy CDN should have 85-95%+ hit ratios for static content. If it is lower, check your Cache-Control headers and Vary configuration.
- Use surrogate keys for API caching: When caching API responses at the CDN, tag-based purging is far more practical than URL-based purging for complex APIs.
Frequently Asked Questions
Can I cache API responses at the CDN?
Yes, and it is increasingly common. Use Cache-Control: public, s-maxage=60 for public API endpoints that return the same data for all users. For personalized APIs, use edge computing to cache per-user responses with user-specific cache keys. Be careful with Vary headers — too many variations fragment the cache and reduce hit ratios.
How do I handle cache warming after a CDN purge?
After a full or large-scale purge, the CDN is cold and all requests hit your origin. Mitigation strategies include: gradual purging (purge regions one at a time), pre-warming by scripting requests to popular URLs after purging, and using stale-while-revalidate so users still get fast (slightly stale) responses during revalidation.
What is the difference between CDN caching and browser caching?
Browser caching stores content on the user's device; CDN caching stores content at edge servers shared by many users in a region. max-age controls browser cache duration; s-maxage controls CDN cache duration. You can set a short browser TTL (5 minutes) with a longer CDN TTL (1 hour) to balance freshness with origin load reduction. Both are part of a layered caching strategy alongside distributed caching with Redis.
How do CDNs handle dynamic content?
CDNs can cache dynamic content with short TTLs (1-60 seconds) for content that changes frequently but can tolerate brief staleness. For truly dynamic, personalized content, CDNs provide edge computing (Cloudflare Workers, Lambda@Edge) to generate responses at the edge without going back to the origin. Some CDNs also support Edge Side Includes (ESI) to cache page fragments independently.