Content Delivery Network (CDN) Pattern
Distribute static and live content through geographically dispersed edge servers to reduce latency, improve availability, and offload origin infrastructure.
Overview
A Content Delivery Network puts cached copies of your content on servers spread around the world, so requests get answered by whichever edge location is closest to the user. The speed gain is mostly physics: a user in Tokyo hitting an origin in Virginia pays roughly 150-200 ms of round-trip latency per request, while a Tokyo edge node answers in about 20 ms. Multiply that across the dozens of requests a page needs and the difference stops being subtle.
Beyond raw distance, a CDN absorbs read traffic so your origin doesn’t have to, blunts traffic spikes and DDoS floods, and increasingly runs code at the edge (Cloudflare Workers, Lambda@Edge, Fastly Compute). What started as “caching for images” now covers TLS termination, bot filtering, geo-routing and, for some apps, full request handling.
When to Use
Use the CDN Pattern when:
- Users are geographically distributed and latency matters
- Static assets (images, CSS, JS, fonts, videos) account for most of your traffic. This pairs naturally with the Static Content Hosting Pattern
- You need to absorb traffic spikes without scaling origin infrastructure
- DDoS protection or WAF functionality is required at the edge
- Edge compute for A/B testing, geo-routing, or authentication gives you something the origin can’t
If your problem is request volume rather than distance, such as a client hammering your API, the Throttling Pattern is the better fit.
When to Avoid
- Your users sit in the same region as the origin. If everyone is already 20 ms from the server, the extra edge hop buys nothing.
- The content can’t be cached and per-user data dominates. Personalized payloads still gain TLS-offload benefits, but you lose the main win.
- Everything is internal. A private CDN in front of an intranet is almost never worth the cost.
- Invalidation complexity outweighs the latency benefit. Publishing dozens of personalized pages per second can turn cache purging into its own engineering problem.
- Edge TLS termination violates your compliance rules; rare, but real for some regulated industries.
Solution
A CDN deployment has three moving parts: the distribution that fronts your origin, the cache-control headers that tell the edge what to store, and the invalidation path for when content changes. The examples below cover all three.
JavaScript (CloudFront + S3 Origin)
// AWS SDK v3 configuration for CDN invalidation
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
const cloudfront = new CloudFrontClient({ region: 'us-east-1' });
/**
* Invalidate CDN cache for specific paths after deployment
*/
async function invalidateCache(distributionId, paths = ['/*']) {
const command = new CreateInvalidationCommand({
DistributionId: distributionId,
InvalidationBatch: {
CallerReference: Date.now().toString(),
Paths: {
Quantity: paths.length,
Items: paths
}
}
});
const response = await cloudfront.send(command);
console.log(`Invalidation created: ${response.Invalidation.Id}`);
return response.Invalidation.Id;
}
// Usage: invalidate after static asset deployment
await invalidateCache('E1234567890ABC', ['/assets/*', '/index.html']);
Python (CDN Cache Warmup + Edge Logic)
import requests
import hashlib
from typing import List, Dict
from concurrent.futures import ThreadPoolExecutor
class CDNManager:
"""Manages CDN interactions for cache warmup, purging, and health checks"""
def __init__(self, base_url: str, api_key: str = None):
self.base_url = base_url.rstrip('/')
self.api_key = api_key
self.edge_locations = [
"us-east", "us-west", "eu-west", "eu-central",
"ap-southeast", "ap-northeast", "sa-east"
]
def generate_cache_key(self, path: str, params: Dict = None) -> str:
"""Generate a deterministic cache key for a URL"""
content = f"{path}:{sorted(params.items()) if params else ''}"
return hashlib.sha256(content.encode()).hexdigest()[:16]
def warmup_cache(self, paths: List[str]) -> Dict[str, bool]:
"""Pre-populate CDN edge caches by requesting through each location"""
results = {}
def fetch(path):
try:
response = requests.get(
f"{self.base_url}{path}",
headers={"X-Cache-Warmup": "true"},
timeout=30
)
return path, response.status_code == 200
except Exception:
return path, False
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(fetch, p) for p in paths]
for future in futures:
path, success = future.result()
results[path] = success
return results
def purge_cache(self, path: str) -> bool:
"""Purge a specific path from the CDN cache"""
# Implementation varies by CDN provider (Fastly, Cloudflare, etc.)
# This example uses a generic purge endpoint
try:
response = requests.post(
f"{self.base_url}/__purge",
json={"path": path},
headers={"Authorization": f"Bearer {self.api_key}"} if self.api_key else {}
)
return response.status_code == 200
except Exception as e:
print(f"Purge failed for {path}: {e}")
return False
def get_cache_status(self, path: str) -> Dict:
"""Check cache HIT/MISS status for a path"""
response = requests.get(f"{self.base_url}{path}")
return {
"path": path,
"cache_status": response.headers.get("X-Cache", "unknown"),
"age": response.headers.get("Age", "0"),
"ttl_remaining": response.headers.get("Cache-Control", "")
}
# Usage
cdn = CDNManager("https://cdn.example.com", api_key="secret-key")
# Warmup critical paths after deployment
warmup_results = cdn.warmup_cache([
"/assets/main.css",
"/assets/app.js",
"/api/config"
])
print(f"Cache warmup: {sum(warmup_results.values())}/{len(warmup_results)} successful")
# Check cache status
status = cdn.get_cache_status("/assets/main.css")
print(f"Cache: {status['cache_status']}, Age: {status['age']}s")
Terraform (AWS CloudFront Distribution)
# Infrastructure as Code for CloudFront CDN
resource "aws_cloudfront_distribution" "cdn" {
enabled = true
is_ipv6_enabled = true
comment = "StackPractices CDN"
default_root_object = "index.html"
price_class = "PriceClass_All" # Global distribution
# Origin: S3 bucket for static assets
origin {
domain_name = aws_s3_bucket.assets.bucket_regional_domain_name
origin_id = "S3-assets"
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.oai.cloudfront_access_identity_path
}
}
# Origin: ALB for live API
origin {
domain_name = aws_lb.main.dns_name
origin_id = "ALB-api"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
}
}
# Default cache behavior: static assets from S3
default_cache_behavior {
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3-assets"
compress = true
forwarded_values {
query_string = false
cookies { forward = "none" }
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 86400 # 1 day
max_ttl = 31536000 # 1 year
}
# Ordered cache behavior: API calls with no caching
ordered_cache_behavior {
path_pattern = "/api/*"
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "ALB-api"
compress = true
forwarded_values {
query_string = true
headers = ["Authorization", "Origin"]
cookies { forward = "all" }
}
viewer_protocol_policy = "https-only"
min_ttl = 0
default_ttl = 0
max_ttl = 0
}
# Edge caching for specific asset types
ordered_cache_behavior {
path_pattern = "/assets/*"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3-assets"
compress = true
forwarded_values {
query_string = true
cookies { forward = "none" }
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 86400 # 1 day
default_ttl = 604800 # 1 week
max_ttl = 31536000 # 1 year
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.cdn.arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
tags = {
Environment = "production"
Service = "cdn"
}
}
Cache-Control Headers
Infrastructure alone isn’t the pattern: the headers your origin sends are the contract that makes the edge cache work:
# Static, fingerprinted assets - safe to cache "forever"
Cache-Control: public, max-age=31536000, immutable
# HTML pages - browsers revalidate, but the CDN may keep a short copy
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=60
# Personalized responses - never store at the edge
Cache-Control: private, no-store
max-age talks to the client, while s-maxage talks to shared caches: the CDN. The classic trick is max-age=0, s-maxage=300: browsers always revalidate so users see fresh HTML, but the edge keeps a five-minute copy that absorbs repeat traffic. stale-while-revalidate goes further and lets the edge serve the stale copy while it refreshes in the background, so a slow origin fetch never reaches a user.
Explanation
How a request actually flows matters for everything that follows. In pull mode (the default on CloudFront, Cloudflare and friends), DNS or anycast routing sends the user to the nearest point of presence. The edge looks up the request in its cache: on a HIT it answers in milliseconds; on a MISS it forwards the request upstream (to a regional shield if one is configured, otherwise straight to your origin), then stores the response according to Cache-Control and serves it to the next identical request for free.
The cache key decides your hit ratio. The CDN hashes the URL plus whatever request inputs you whitelist (query parameters, headers, cookies) to decide whether two requests are “the same object.” Whitelist nothing by default. Every extra input multiplies the number of cached variants, and query params like ?utm_source=newsletter are the classic hit-ratio killer: strip tracking parameters at the edge or they’ll fragment your cache into thousands of single-use entries.
Three timers cooperate on freshness. max-age is for browsers, s-maxage is for shared caches, and stale-while-revalidate / stale-if-error buy resilience during origin trouble. CDN-side TTL configuration is a fallback for when the origin sends no headers, but origin headers should be the source of truth, because they’re versioned with your deploys instead of sitting in a dashboard. The Complete Guide to CDN Caching Strategy covers TTL and key design in depth.
Plan invalidation before you need it. Fingerprinted URLs (app.a3f8b2.js) are the clean answer, because the URL changes when the content does, so nothing needs purging. For stable URLs, call the provider’s purge API from the deploy pipeline; path and wildcard invalidations propagate in seconds to minutes depending on the vendor. Tag-based purging (Fastly surrogate keys, Cloudflare cache tags) is the precise option when many objects expire together.
Origin shielding collapses stampedes. Without it, fifty PoPs all miss at once on a deploy and your origin eats fifty concurrent fetches. A designated shield tier turns that into one fetch, which is also why shielding pairs well with health checks on the origin itself.
Variants
| Variant | Use Case | Example |
|---|---|---|
| Static asset CDN | Images, CSS, JS, fonts | CloudFront + S3 |
| Edge acceleration | API responses, HTML pages | Cloudflare Argo, Fastly |
| Video streaming | HLS/DASH segments, live streams | AWS MediaPackage, Akamai |
| Edge computing | A/B testing, auth, personalization | Cloudflare Workers, Lambda@Edge |
| Multi-CDN | Resilience and cost optimization | CloudFront + Fastly failover |
What Works
- Fingerprint assets and cache them forever.
app.a3f8b2.jswithmax-age=31536000, immutablemeans deploys get a new URL and you never purge. This single habit removes most invalidation pain. - Match TTL to volatility, not to a default. Versioned assets get a year; APIs get seconds based on staleness tolerance; HTML gets a short
s-maxageplus revalidation. - Keep the cache key minimal. Whitelist query params, headers and cookies deliberately. Every input you add multiplies variants and drags the hit ratio down.
- Plan the fallback before launch. The origin has to stay healthy enough to serve if the CDN misses or dies: monitor it directly, not through the CDN.
- Watch cache-hit ratio, not just latency. Below ~80% almost always means a key or TTL problem, not a traffic problem. Fastly and Cloudflare both expose it per path.
Common Mistakes
- Forgetting the invalidation path. Classic post-deploy bug: new HTML references
app.v2.jsbut the CDN still serves the old HTML because nobody purged it. Automate the purge in the deploy pipeline. - Caching personalized pages publicly. Missing
private/no-storemeans one user’s dashboard can be served to the next. Most CDNs bypass cache when they seeSet-CookieorAuthorization; verify that yours does. - Letting tracking params fragment the cache.
utm_*,fbclid,gclideach mint a new cache entry for identical content. Strip them at the edge. - Compressing only at the origin. Enable edge compression (Brotli where the client supports it) and let the CDN negotiate per client.
- Treating the CDN as a proxy. If every request misses, you’ve built an extra hop with a bill attached. A healthy hit ratio is what does the work.
Real-World Examples
Netflix Open Connect
Netflix runs its own CDN appliances inside ISP networks. Putting content inside the ISP’s own infrastructure cuts transit costs and delivers 4K streams with minimal buffering. It’s a reminder that at enough scale, “use a CDN” becomes “be a CDN.”
StackPractices.com
This site runs on GitHub Pages with Cloudflare in front. Static HTML, CSS and JS are cached at Cloudflare’s edge, which keeps origin load near zero and load times consistent worldwide.
Shopify Storefronts
Shopify serves millions of storefronts through Fastly. Theme assets cached at edge locations deliver sub-second page loads globally even though the origin platform is centralized; edge caching is what makes that architecture viable at all.
See Also
- Complete Guide: CDN Caching Strategy — TTLs, cache keys and invalidation in depth
- CDN Cache Rules Template — a ready-to-adapt cache policy document
- Companion repository — runnable artifacts: CloudFront Terraform, cache warmup/purge tooling, and Cache-Control recipes
- AWS CloudFront caching documentation
- RFC 9111: HTTP Caching
Frequently Asked Questions
Does a CDN only work for static content?
No. That was true in the early Akamai era. Modern CDNs accelerate live responses by terminating TLS closer to the user, keeping warm connections back to your origin, and routing over private backbone links instead of the public internet. A cache miss still costs a round trip, but it's a faster one. Edge compute goes further and can answer without an origin call at all.
How do I handle cache invalidation?
The best invalidation is the one you never do: fingerprint static assets (app.a3f8b2.js) and the URL changes when the content does. For stable URLs, call the CDN's purge API from your deploy pipeline (path or wildcard). Tag-based purging (Fastly surrogate keys, Cloudflare cache tags) is the precise option when many related objects expire together.
What is the difference between pull and push CDN?
A pull CDN fetches from your origin on the first request and caches the result, so there's zero upfront work, and only the first visitor per edge location pays origin latency. A push CDN makes you upload content yourself; it's only worth it for very large files or content that rarely changes. Nearly every modern CDN is pull with optional origin shielding.
What is the difference between max-age and s-maxage?
max-age applies to the client; s-maxage applies to shared caches like the CDN and wins there when both are present. Cache-Control: max-age=0, s-maxage=300 means "browser, always revalidate; edge, keep this for five minutes". That's the standard trick for HTML that must stay fresh for users but cacheable at the edge.
What happens if the CDN goes down?
Less than you fear, more than you hope. PoPs are massively redundant, but full-provider outages happen: the Fastly outage in June 2021 took down a large slice of the web for about an hour. Mitigations: an origin healthy enough to serve directly, DNS or health-check failover, and for the truly paranoid, a second CDN in multi-CDN mode.
Should I use a CDN for an internal application?
Usually no. If users sit on a corporate network a short hop from the origin, the edge adds a middleman with no latency win. The exceptions: offices spread across continents, or using the edge to terminate TLS and auth before traffic reaches your network.
Related Resources
Static Content Hosting Pattern
Deploy static files to a dedicated content delivery network or object storage to offload origin servers, reduce latency, and improve availability for assets like images, CSS, and JavaScript.
PatternThrottling Pattern
Limit the rate at which a system processes requests or consumes resources to prevent overload, ensure fair usage, and maintain predictable performance under varying load.
GuideComplete Guide to CDN Caching Strategy
CDN caching for web apps and APIs: edge caching, cache keys, headers, invalidation, surrogate keys, stale strategies and multi-CDN setups.
PatternGatekeeper Pattern: Centralized Edge Security
Centralize authentication, rate limiting, and input sanitization at the system edge. Gatekeeper Pattern with Python, Java, and JavaScript examples.
PatternCompute Resource Consolidation Pattern
Combine workloads into fewer compute resources to reduce cost, improve utilization, and simplify operations.
PatternHealth Endpoint Monitoring Pattern
Expose lightweight health endpoints so orchestrators, load balancers, and monitoring tools can verify service availability.