Skip to content

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 requests

Request Flow

  1. DNS resolution — The CDN’s DNS returns the IP of the nearest edge server (using anycast or geo-based DNS routing).
  2. Edge check — The edge server checks whether it has a cached copy of the requested resource.
  3. Cache HIT — If the resource is cached and not expired, the edge server returns it directly. Round-trip time is typically under 10ms.
  4. 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 ProviderApproximate PoPsNotable Feature
Cloudflare310+ citiesIntegrated WAF and DDoS protection
AWS CloudFront450+ PoPsDeep AWS integration
Akamai4,000+ locationsLargest network
Fastly90+ PoPsReal-time purging, edge compute (Wasm)
Google Cloud CDN180+ PoPsUses 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)
DirectiveMeaning
publicResponse can be cached by any cache (browser, CDN, proxy)
privateResponse is specific to one user; only browser may cache
max-age=NCache is fresh for N seconds
s-maxage=NLike max-age but only for shared caches (CDN/proxy); overrides max-age
no-cacheCache may store, but must revalidate with origin on every request
no-storeDo not store the response anywhere (for sensitive data)
must-revalidateOnce stale, must revalidate before reuse; do not serve stale
stale-while-revalidate=NServe stale for N seconds while revalidating in the background
immutableResource 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

FeatureETagLast-Modified
PrecisionByte-level (hash-based)Second-level (timestamp)
TypeStrong or weakTimestamp only
Best forDynamic content, APIsStatic files
OverheadServer must compute hashFilesystem 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.js
After: /assets/app.a1b2c3d4.js
Webpack/Vite output:
app.a1b2c3d4.js -- changes when code changes
vendor.e5f6g7h8.js -- changes only when dependencies change

2. Query string versioning

Append a version or timestamp as a query parameter.

/assets/style.css?v=2.1.0
/assets/style.css?t=1705312200

3. Path versioning

Include the version in the URL path.

/v2/assets/style.css
/build-1234/app.js
# Flask example: serving hashed static assets
import hashlib
import os
from 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_request
def 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

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 origin

Edge 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

PlatformRuntimeLanguage Support
Cloudflare WorkersV8 isolatesJavaScript, TypeScript, Wasm
AWS Lambda@EdgeNode.js containersJavaScript, Python
Fastly ComputeWebAssemblyRust, Go, JavaScript
Deno DeployV8 isolatesTypeScript, JavaScript
Vercel Edge FunctionsV8 isolatesJavaScript, 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 example
export 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;
}
};

CDN Best Practices

Common Mistakes

MistakeConsequenceFix
No cache headers on static assetsEvery request hits originAdd max-age and immutable
Set-Cookie on cached responsesCDN caches personalized dataMove cookies to API routes; strip from static
Long max-age on HTML without hashed asset URLsUsers see stale HTML pointing to old assetsUse no-cache for HTML, hashed filenames for assets
Caching error responses (5xx)Users see error pages from cacheUse Cache-Control: no-store on error responses
No Vary header for content negotiationWrong format served (e.g., WebP to Safari)Add Vary: Accept for format negotiation

Summary

ConceptKey Takeaway
CDNDistributed cache that serves content from the nearest edge location
Cache-ControlThe primary HTTP header for controlling cache behavior
ETagEnables conditional requests to avoid transferring unchanged resources
Cache bustingFilename hashing is the most reliable strategy
Origin shieldReduces origin load by adding an intermediate cache layer
Stale-while-revalidateServes stale content immediately while refreshing in the background
Edge computingRuns application logic at CDN edge locations for ultra-low latency