CDN Cache Architecture
A CDN (Content Delivery Network) operates a global network of Points of Presence (PoPs) — data centers positioned close to end users. When a user requests a resource, the request routes to the nearest PoP rather than your origin server.
User → Nearest PoP → Cache HIT? → Return cached response
↓ Cache MISS
Origin Shield (optional) → Cache HIT? → Return
↓ Cache MISS
Origin Server
Origin Shield is an optional intermediate cache tier between edge PoPs and your origin. All edge PoPs that miss their local cache funnel through the shield, preventing thousands of simultaneous cache-miss requests (a "thundering herd") from hitting your origin.
Cache Key Design
A cache key is the unique identifier the CDN uses to look up a cached response. By default, most CDNs use only the full URL as the cache key. But the right cache key depends on your content.
The Vary Header Trap
The HTTP Vary response header tells CDNs to include specific request headers in the cache key:
Vary: Accept-Encoding
This is correct — the CDN should cache separate copies for gzip and brotli responses. But Vary: Accept or Vary: Cookie can shatter your cache — every user gets their own cache entry, hit rate drops to near zero.
# Bad: Vary on Cookie means every user session is a separate cache key
response['Vary'] = 'Cookie'
# Better: Strip the cookie from CDN cache key, vary only on Accept-Encoding
response['Vary'] = 'Accept-Encoding'
response['Cache-Control'] = 'public, max-age=3600'
Query Parameter Normalization
The URLs /search?q=python&page=1 and /search?page=1&q=python are identical content but different cache keys by default. Configure your CDN to sort query parameters before computing the cache key:
# Nginx: sort query parameters before proxying to cache
proxy_cache_key "$scheme$request_method$host$uri$is_args$args";
Most CDNs (Cloudflare, CloudFront) offer query string normalization in their dashboards.
Cache Invalidation
Cache invalidation is notoriously difficult. You need to purge cached content when it changes, but purge too aggressively and you lose the performance benefit.
URL Purge
The simplest approach: purge by exact URL.
# Cloudflare: purge a single file
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
--data '{"files": ["https://example.com/article/my-post"]}'
URL purges work well for individual resources. They fail when a single change affects hundreds of pages (e.g., updating a navigation component).
Tag-Based Purge (Surrogate Keys)
Tag-based purging lets you associate a cache entry with one or more tags and then purge all entries sharing a tag in a single API call.
# Django view — tag the response with content identifiers
def article_detail(request, slug):
article = get_object_or_404(Article, slug=slug)
response = render(request, 'article.html', {'article': article})
response['Cache-Control'] = 'public, max-age=3600'
# Cloudflare Cache-Tag header (max 16KB)
tags = f'article-{article.id},author-{article.author_id},category-{article.category_id}'
response['Cache-Tag'] = tags
return response
# When an author changes their name — purge all their articles
def purge_author_content(author_id: int):
requests.post(
f'https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/purge_cache',
headers={'Authorization': f'Bearer {CF_TOKEN}'},
json={'tags': [f'author-{author_id}']},
)
| Provider | Tag Header | Purge API |
|---|---|---|
| Cloudflare | `Cache-Tag` | `/purge_cache` with `tags` |
| Fastly | `Surrogate-Key` | `/service/{id}/purge/{key}` |
| Varnish | `Surrogate-Key` | `PURGE` + `X-Purge-Tag` |
| CloudFront | Via invalidation paths only | CreateInvalidation API |
Advanced Patterns
stale-while-revalidate
The stale-while-revalidate directive serves stale content immediately while revalidating in the background. Users never wait for revalidation:
Cache-Control: public, max-age=60, stale-while-revalidate=3600
This means: serve from cache for 60 seconds. After 60s, serve the stale cached copy immediately AND revalidate in the background. The next request after revalidation completes gets the fresh content.
stale-if-error
Cache-Control: public, max-age=300, stale-if-error=86400
If the origin returns a 5xx error, serve the stale cached response for up to 24 hours. This is your last line of defense against origin outages affecting end users.
Provider Comparison
Cloudflare: Cache Rules (formerly Page Rules) for URL-pattern caching. Workers run JavaScript at the edge — you can modify requests/responses, implement custom cache keys, or serve synthetic responses. Cache Analytics dashboard shows hit ratio per URL pattern.
AWS CloudFront: Cache Behaviors configure which URLs use which cache settings. Origin Request Policies control which headers/cookies/query strings reach the origin. Lambda@Edge runs Node.js code at edge locations (4 trigger points). CloudFront Functions are cheaper for simple header manipulation.
Fastly: VCL (Varnish Configuration Language) gives low-level control over cache behavior. Instant Purge API is one of the fastest in the industry (~150ms global). Compute@Edge runs Rust/JavaScript/Go at the edge.
Key Takeaways
- Set
Cache-Control: public, max-age=Non static assets — CDNs will not cache without it - Avoid
Vary: CookieorVary: Accept— they fragment the cache by user - Use tag-based purging (Surrogate-Key) for content that is referenced by many pages
stale-while-revalidatedelivers perceived instant updates without cache stampedesstale-if-errorprovides resilience during origin outages at zero cost