CDN & Edge Caching
A Content Delivery Network (CDN) is a geographically distributed network of servers that caches content closer to end users. By serving assets from the nearest edge location instead of a distant origin server, CDNs reduce latency, decrease origin load, and improve availability.
How CDNs Work
Without CDN: User (Tokyo) ──── 150ms ────► Origin (US-East)
With CDN: User (Tokyo) ──── 5ms ────► Edge (Tokyo) ──── cache HIT ──► response │ cache MISS │ ▼ Origin (US-East) Response cached at edge for subsequent requestsRequest Flow
- DNS resolution — The CDN’s DNS returns the IP of the nearest edge server (using anycast or geo-based DNS routing).
- Edge check — The edge server checks whether it has a cached copy of the requested resource.
- Cache HIT — If the resource is cached and not expired, the edge server returns it directly. Round-trip time is typically under 10ms.
- Cache MISS — If the resource is not cached, the edge server fetches it from the origin, caches the response, and returns it to the user.
┌────────────────────────────────────────────────────────┐│ CDN Architecture ││ ││ ┌─────────┐ ┌─────────┐ ┌─────────┐ ││ │ Edge │ │ Edge │ │ Edge │ ││ │ Tokyo │ │ London │ │ Sao Paulo│ ││ └────┬────┘ └────┬────┘ └────┬────┘ ││ │ │ │ ││ └───────────────┼───────────────┘ ││ │ ││ ┌────┴────┐ ││ │ Origin │ ││ │ Server │ ││ └─────────┘ │└────────────────────────────────────────────────────────┘Edge Locations and PoPs
A Point of Presence (PoP) is a physical data center where CDN edge servers are located. Major CDN providers operate hundreds of PoPs worldwide:
| CDN Provider | Approximate PoPs | Notable Feature |
|---|---|---|
| Cloudflare | 310+ cities | Integrated WAF and DDoS protection |
| AWS CloudFront | 450+ PoPs | Deep AWS integration |
| Akamai | 4,000+ locations | Largest network |
| Fastly | 90+ PoPs | Real-time purging, edge compute (Wasm) |
| Google Cloud CDN | 180+ PoPs | Uses Google’s backbone |
HTTP Cache Headers
HTTP cache headers tell browsers and CDNs how to cache responses. Mastering these headers is essential for controlling cache behavior.
Cache-Control
The Cache-Control header is the primary mechanism for controlling caching. It supports multiple directives.
Most common directives:
Cache-Control: public, max-age=31536000 │ │ │ │ │ └─ Cache for 1 year (in seconds) │ └─ Any cache (browser, CDN, proxy) may store └─ The header name
Cache-Control: private, no-cache │ │ │ │ │ └─ Must revalidate with origin before use │ └─ Only browser may cache, not CDN/proxy └─ The header name
Cache-Control: no-store └─ Do not cache at all (sensitive data)| Directive | Meaning |
|---|---|
public | Response can be cached by any cache (browser, CDN, proxy) |
private | Response is specific to one user; only browser may cache |
max-age=N | Cache is fresh for N seconds |
s-maxage=N | Like max-age but only for shared caches (CDN/proxy); overrides max-age |
no-cache | Cache may store, but must revalidate with origin on every request |
no-store | Do not store the response anywhere (for sensitive data) |
must-revalidate | Once stale, must revalidate before reuse; do not serve stale |
stale-while-revalidate=N | Serve stale for N seconds while revalidating in the background |
immutable | Resource will never change; skip revalidation entirely |
ETag (Entity Tag)
An ETag is a unique identifier for a specific version of a resource. It enables conditional requests so the server can say “the resource has not changed” without sending the full response body.
First request: GET /api/users/42 Response: 200 OK ETag: "abc123" Body: ... (full response)
Subsequent request (conditional): GET /api/users/42 If-None-Match: "abc123"
If unchanged: 304 Not Modified <-- no body, saves bandwidth If changed: 200 OK ETag: "def456" Body: ... (new response)Last-Modified / If-Modified-Since
Similar to ETag but uses timestamps instead of hashes.
First request: GET /style.css Response: 200 OK Last-Modified: Wed, 15 Jan 2025 10:30:00 GMT Body: ... (full file)
Subsequent request: GET /style.css If-Modified-Since: Wed, 15 Jan 2025 10:30:00 GMT
If unchanged: 304 Not Modified If changed: 200 OK Last-Modified: Thu, 20 Feb 2025 08:00:00 GMT Body: ... (new file)ETag vs Last-Modified
| Feature | ETag | Last-Modified |
|---|---|---|
| Precision | Byte-level (hash-based) | Second-level (timestamp) |
| Type | Strong or weak | Timestamp only |
| Best for | Dynamic content, APIs | Static files |
| Overhead | Server must compute hash | Filesystem provides timestamp |
Cache Busting
Cache busting forces browsers and CDNs to fetch a new version of a resource, bypassing the cached version. This is necessary when you deploy new code but the old version is cached with a long max-age.
Strategies
1. Filename hashing (recommended)
Embed a content hash in the filename. When the file changes, the hash changes, creating a new URL.
Before: /assets/app.jsAfter: /assets/app.a1b2c3d4.js
Webpack/Vite output: app.a1b2c3d4.js -- changes when code changes vendor.e5f6g7h8.js -- changes only when dependencies change2. Query string versioning
Append a version or timestamp as a query parameter.
/assets/style.css?v=2.1.0/assets/style.css?t=17053122003. Path versioning
Include the version in the URL path.
/v2/assets/style.css/build-1234/app.js# Flask example: serving hashed static assetsimport hashlibimport osfrom flask import Flask, url_for
app = Flask(__name__)
def hashed_url(filename): """Generate a URL with content hash for cache busting.""" filepath = os.path.join(app.static_folder, filename) with open(filepath, 'rb') as f: file_hash = hashlib.md5(f.read()).hexdigest()[:8] return url_for('static', filename=f"{file_hash}/{filename}")
# In a Jinja template:# <link rel="stylesheet" href="{{ hashed_url('style.css') }}"># Output: /static/a1b2c3d4/style.css
# Setting cache headers in Flask@app.after_requestdef add_cache_headers(response): if response.content_type.startswith('text/html'): response.headers['Cache-Control'] = ( 'public, max-age=0, must-revalidate' ) elif '/static/' in response.headers.get('Location', ''): response.headers['Cache-Control'] = ( 'public, max-age=31536000, immutable' ) return response// Express.js: setting cache headersconst express = require('express');const app = express();
// Static assets with long cache + immutableapp.use('/assets', express.static('dist/assets', { maxAge: '1y', immutable: true, setHeaders: (res, path) => { // Hashed filenames are safe to cache forever if (path.match(/\.[a-f0-9]{8}\./)) { res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); } }}));
// HTML pages: no-cache (always revalidate)app.get('*', (req, res) => { res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate'); res.sendFile('index.html');});
// CDN purge via API (Cloudflare example)const fetch = require('node-fetch');async function purgeCloudflareCache(urls) { const response = await fetch( `https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache`, { method: 'POST', headers: { 'Authorization': `Bearer ${API_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ files: urls }) } ); return response.json();}import org.springframework.context.annotation.*;import org.springframework.web.servlet.config.annotation.*;
@Configurationpublic class CacheConfig implements WebMvcConfigurer {
@Override public void addResourceHandlers( ResourceHandlerRegistry registry) { // Hashed static assets: cache for 1 year registry.addResourceHandler("/assets/**") .addResourceLocations("classpath:/static/assets/") .setCachePeriod(31536000) .resourceChain(true) .addResolver( new VersionResourceResolver() .addContentVersionStrategy("/**") ); }}
// Controller: set cache headers for HTML@GetMapping("/")public ResponseEntity<String> index() { return ResponseEntity.ok() .cacheControl(CacheControl .noCache() .mustRevalidate()) .body(renderTemplate("index"));}CDN Configuration Patterns
Origin Shield
An origin shield is an intermediate cache layer between edge servers and the origin. Instead of every edge server fetching from the origin on a miss, they fetch from the shield. This drastically reduces origin load.
Without shield: Edge (Tokyo) ──► Origin Edge (London) ──► Origin (3 requests to origin) Edge (Sydney) ──► Origin
With shield: Edge (Tokyo) ──► Shield (US-West) ──► Origin Edge (London) ──► Shield (US-West) (1 request) Edge (Sydney) ──► Shield (US-West)Stale-While-Revalidate
This pattern serves the stale cached version immediately while fetching a fresh version in the background. Users always get a fast response, and the cache is updated asynchronously.
Cache-Control: public, max-age=60, stale-while-revalidate=300
Timeline: 0-60s: Serve from cache (fresh) 60-360s: Serve stale immediately, revalidate in background 360s+: Cache expired, must fetch from originEdge Computing
Edge computing moves application logic from centralized servers to CDN edge locations, running code closer to users. This reduces latency for dynamic content that cannot be simply cached.
Edge Computing Platforms
| Platform | Runtime | Language Support |
|---|---|---|
| Cloudflare Workers | V8 isolates | JavaScript, TypeScript, Wasm |
| AWS Lambda@Edge | Node.js containers | JavaScript, Python |
| Fastly Compute | WebAssembly | Rust, Go, JavaScript |
| Deno Deploy | V8 isolates | TypeScript, JavaScript |
| Vercel Edge Functions | V8 isolates | JavaScript, TypeScript |
Use Cases
- A/B testing — Route users to different variants at the edge without hitting the origin
- Authentication — Validate JWTs at the edge and reject unauthorized requests before they reach the origin
- Personalization — Modify HTML at the edge based on geolocation or cookies
- API gateway — Rate limiting, request routing, and header manipulation at the edge
- Image optimization — Resize and format images on-the-fly at the edge
// Cloudflare Worker exampleexport default { async fetch(request, env) { const url = new URL(request.url);
// Geolocation-based routing const country = request.cf?.country || 'US';
// A/B testing at the edge const bucket = request.headers.get('cookie') ?.match(/ab_bucket=(\w+)/)?.[1] || (Math.random() > 0.5 ? 'A' : 'B');
// Modify origin request const originUrl = bucket === 'A' ? 'https://origin-a.example.com' : 'https://origin-b.example.com';
const response = await fetch( `${originUrl}${url.pathname}`, { headers: request.headers } );
// Add custom headers const newResponse = new Response(response.body, response); newResponse.headers.set('X-Edge-Location', country); newResponse.headers.set('X-AB-Bucket', bucket); newResponse.headers.set( 'Cache-Control', 'public, s-maxage=60' );
return newResponse; }};# AWS Lambda@Edge example (viewer request)import jsonimport base64
def lambda_handler(event, context): """ Lambda@Edge function for A/B testing. Runs at CloudFront edge locations. """ request = event['Records'][0]['cf']['request'] headers = request['headers']
# Determine A/B bucket from cookie cookies = headers.get('cookie', [{}]) cookie_str = cookies[0].get('value', '') if cookies else ''
if 'ab_bucket=A' in cookie_str: bucket = 'A' elif 'ab_bucket=B' in cookie_str: bucket = 'B' else: # New visitor: assign randomly import random bucket = 'A' if random.random() > 0.5 else 'B'
# Route to different origin based on bucket if bucket == 'B': request['origin'] = { 'custom': { 'domainName': 'origin-b.example.com', 'port': 443, 'protocol': 'https', 'path': '', 'sslProtocols': ['TLSv1.2'], 'readTimeout': 30, 'keepaliveTimeout': 5 } }
# Add bucket header for downstream tracking request['headers']['x-ab-bucket'] = [{ 'key': 'X-AB-Bucket', 'value': bucket }]
return requestCDN Best Practices
Common Mistakes
| Mistake | Consequence | Fix |
|---|---|---|
| No cache headers on static assets | Every request hits origin | Add max-age and immutable |
Set-Cookie on cached responses | CDN caches personalized data | Move cookies to API routes; strip from static |
Long max-age on HTML without hashed asset URLs | Users see stale HTML pointing to old assets | Use no-cache for HTML, hashed filenames for assets |
| Caching error responses (5xx) | Users see error pages from cache | Use Cache-Control: no-store on error responses |
No Vary header for content negotiation | Wrong format served (e.g., WebP to Safari) | Add Vary: Accept for format negotiation |
Summary
| Concept | Key Takeaway |
|---|---|
| CDN | Distributed cache that serves content from the nearest edge location |
| Cache-Control | The primary HTTP header for controlling cache behavior |
| ETag | Enables conditional requests to avoid transferring unchanged resources |
| Cache busting | Filename hashing is the most reliable strategy |
| Origin shield | Reduces origin load by adding an intermediate cache layer |
| Stale-while-revalidate | Serves stale content immediately while refreshing in the background |
| Edge computing | Runs application logic at CDN edge locations for ultra-low latency |