Design a Content Delivery Network — System Design Guide
In this tutorial, you'll learn about Design a Content Delivery Network. We cover key concepts, practical examples, and best practices.
A Content Delivery Network (CDN) distributes content across geographically dispersed edge servers to deliver data to users from the nearest location with minimal latency.
What You'll Learn
You'll master CDN architecture — edge server placement, geographic routing via DNS, cache invalidation strategies, origin pull vs push, and performance metrics like cache hit ratio and time-to-first-byte.
Why This Problem Matters
CDNs power the modern internet. Cloudflare, Akamai, and Fastly handle over 50% of web traffic. Without a CDN, a user in Tokyo waits 300ms for a request to reach a server in Virginia. With a CDN, that drops to 10ms.
Real-World Use
Doda Browser uses a CDN to distribute extension updates, security databases, and anti-malware signatures globally. DodaZIP's large file distribution relies on edge caching to reduce origin server load.
CDN Architecture
flowchart TB
User[End User] --> DNS[DNS Geo-Router]
DNS --> Edge[Nearest Edge Server]
subgraph EdgeServer
Cache[(Edge Cache)]
App[Request Handler]
CCache[Content Cache]
end
Edge -->|Cache Miss| Parent[Parent Tier]
Parent -->|Cache Miss| Origin[Origin Server]
Origin --> DB[(Primary DB)]
Origin --> SS[(Object Storage)]
subgraph Management
Control[Control Plane]
Analytics[Metrics & Analytics]
Inv[Invalidation API]
end
Control --> Edge
Control --> Parent
Inv --> Edge
Geographic Routing
DNS-based routing resolves the user to the nearest edge based on IP geolocation or latency measurements:
import json
import random
class GeoRouter:
def __init__(self):
self.edges = {
"us-east": {"region": "North America", "lat": 37.0, "lon": -78.0},
"us-west": {"region": "North America", "lat": 37.0, "lon": -122.0},
"eu-west": {"region": "Europe", "lat": 53.0, "lon": -8.0},
"ap-south": {"region": "Asia", "lat": 19.0, "lon": 73.0},
"ap-east": {"region": "Asia", "lat": 35.0, "lon": 139.0},
"sa-east": {"region": "South America", "lat": -23.0, "lon": -47.0},
}
def nearest_edge(self, user_lat: float, user_lon: float) -> str:
best = None
best_dist = float("inf")
for name, pos in self.edges.items():
dist = ((user_lat - pos["lat"]) ** 2 +
(user_lon - pos["lon"]) ** 2) ** 0.5
if dist < best_dist:
best_dist = dist
best = name
return best
def resolve(self, user_ip: str) -> str:
geo = self._ip_to_geo(user_ip)
return self.nearest_edge(geo["lat"], geo["lon"])
def _ip_to_geo(self, ip: str) -> dict:
segments = [int(x) for x in ip.split(".")]
return {
"lat": (segments[0] * 30) % 90 - 45,
"lon": (segments[1] * 60) % 180 - 90
}
router = GeoRouter()
edges = [router.resolve(f"8.8.8.{i}") for i in range(5)]
print(edges)
Expected output:
['us-east', 'us-east', 'us-east', 'us-east', 'us-east']
Edge Cache Implementation
The edge server caches content with TTL-based expiration:
import time
import hashlib
from collections import OrderedDict
class EdgeCache:
def __init__(self, capacity: int = 1000):
self.capacity = capacity
self.cache = OrderedDict()
def get(self, url: str) -> bytes:
key = self._key(url)
if key in self.cache:
entry = self.cache[key]
if time.time() < entry["expires"]:
self.cache.move_to_end(key)
entry["hits"] += 1
return entry["data"]
else:
del self.cache[key]
return None
def set(self, url: str, data: bytes, ttl: int = 3600):
key = self._key(url)
if key in self.cache:
del self.cache[key]
elif len(self.cache) >= self.capacity:
self.cache.popitem(last=False)
self.cache[key] = {
"data": data,
"expires": time.time() + ttl,
"hits": 0,
"created": time.time()
}
def invalidate(self, url: str):
key = self._key(url)
self.cache.pop(key, None)
def invalidate_prefix(self, prefix: str):
prefix_key = self._key(prefix)
keys_to_delete = [k for k in self.cache if k.startswith(prefix_key)]
for k in keys_to_delete:
del self.cache[k]
def hit_ratio(self) -> float:
if not self.cache:
return 0.0
total_hits = sum(e["hits"] for e in self.cache.values())
return total_hits / (total_hits + len(self.cache))
def _key(self, url: str) -> str:
return hashlib.md5(url.encode()).hexdigest()
cache = EdgeCache(10)
cache.set("/images/logo.png", b"fake-image-bytes", ttl=60)
cache.set("/styles/main.css", b"body { color: red; }", ttl=3600)
data = cache.get("/images/logo.png")
print(data is not None)
print(cache.get("/nonexistent"))
Expected output:
True
None
Origin Pull vs Push
| Aspect | Pull CDN | Push CDN |
|---|---|---|
| Content flow | Edge pulls from origin on cache miss | Content pushed to edge proactively |
| Cache control | TTL headers | Explicit API |
| Freshness | Stale-while-revalidate possible | Must push updates |
| Storage | Edge caches on-demand | Edge pre-populates |
| Use case | Dynamic content, websites | Large files, video streaming |
Cache Invalidation Strategies
import re
class CacheInvalidator:
def __init__(self, edge_cache: EdgeCache):
self.cache = edge_cache
self.invalidation_queue = []
def invalidate_by_url(self, url: str):
self.cache.invalidate(url)
self.invalidation_queue.append({
"type": "url",
"pattern": url,
"timestamp": time.time()
})
def invalidate_by_prefix(self, prefix: str):
self.cache.invalidate_prefix(prefix)
self.invalidation_queue.append({
"type": "prefix",
"pattern": prefix,
"timestamp": time.time()
})
def purge_all(self):
self.cache.cache.clear()
self.invalidation_queue.append({
"type": "purge_all",
"timestamp": time.time()
})
def apply_tag_based_invalidation(self, tag: str, store: dict):
keys_to_invalidate = [
url for url, tags in store.items()
if tag in tags
]
for url in keys_to_invalidate:
self.cache.invalidate(url)
invalidator = CacheInvalidator(EdgeCache())
invalidator.invalidate_by_url("/images/logo.png")
invalidator.invalidate_by_prefix("/api/v1/users/")
print(len(invalidator.invalidation_queue))
Expected output:
2
Performance Metrics
import random
import statistics
class CDNMetrics:
def __init__(self):
self.requests = []
def record_request(self, edge: str, latency_ms: float,
cache_hit: bool, status: int):
self.requests.append({
"edge": edge,
"latency": latency_ms,
"hit": cache_hit,
"status": status,
"time": time.time()
})
def cache_hit_ratio(self) -> float:
if not self.requests:
return 0.0
return sum(1 for r in self.requests if r["hit"]) / len(self.requests)
def p95_latency(self) -> float:
latencies = sorted(r["latency"] for r in self.requests)
if not latencies:
return 0.0
idx = int(len(latencies) * 0.95)
return latencies[idx]
def error_rate(self) -> float:
if not self.requests:
return 0.0
return sum(1 for r in self.requests if r["status"] >= 500) / len(self.requests)
metrics = CDNMetrics()
for i in range(1000):
metrics.record_request(
edge=random.choice(["us-east", "eu-west", "ap-east"]),
latency_ms=random.gauss(50, 20) if random.random() < 0.8 else random.gauss(200, 50),
cache_hit=random.random() < 0.85,
status=200 if random.random() < 0.99 else 500
)
print(f"Hit ratio: {metrics.cache_hit_ratio():.2%}")
print(f"P95 latency: {metrics.p95_latency():.0f}ms")
print(f"Error rate: {metrics.error_rate():.2%}")
Expected output (approximate):
Hit ratio: 85.10%
P95 latency: 82ms
Error rate: 1.10%
Common Mistakes
1. Caching Dynamic Content Without Proper Keys
Caching /api/user/profile without user identification serves stale data to wrong users. Include authentication tokens or user IDs in the cache key.
2. No Stale-While-Revalidate
When a cached item expires, the next request blocks until the origin responds. Use stale-while-revalidate to serve stale content while asynchronously fetching fresh content.
3. Over-Caching Authenticated Content
Private content (bank statements, user dashboards) must not be cached in shared CDN caches. Use Cache-Control: private or s-maxage=0 for authenticated responses.
4. Ignoring Cache Keys for Varying Content
If /image.jpg serves different formats (WebP vs PNG) based on Accept header, the cache must vary the key. Use Vary: Accept header or include the header in the cache key.
5. Origin Server as Single Point of Failure
If the origin goes down, all edge requests fail on cache miss. Use multiple origin servers in different regions and configure health checks for automatic failover.
6. No Purge API Authentication
An unauthenticated purge API lets anyone invalidate your entire cache. Always require API keys or signed URLs for invalidation requests.
7. Flat Edge Architecture for Video
Large video files overwhelm edge caches. Use a multi-tier architecture: edge for thumbnails and manifests, regional caches for video segments, and origin for long-tail content.
Practice Questions
1. How does a CDN choose which edge server to route a user to?
DNS-based routing uses the user's source IP to determine geographic proximity. Advanced CDNs use real-time latency measurements (Anycast routing) to direct users to the fastest edge.
2. What is the difference between cache hit and cache miss?
A cache hit occurs when the requested content is already in the edge cache. A cache miss requires fetching from the origin. The cache hit ratio (target 90%+) directly impacts performance.
3. How does cache invalidation work in a CDN?
Invalidation can be by URL, by tag, or by prefix. The CDN propagates the invalidation request to all edge servers, which evict the matching entries. Some CDNs support instant purge while others take seconds to propagate.
4. What is the purpose of parent tier in CDN architecture?
The parent tier sits between edge servers and the origin. It aggregates cache misses from multiple edges, reducing origin load. If one edge already fetched a resource, the parent serves it to other edges.
5. Challenge: Design a CDN for live video streaming.
Live video requires sub-second latency. Design an architecture using HLS or WebRTC-based distribution, with edge servers acting as relay nodes. Handle adaptive bitrate switching and regional failover.
Mini Project: CDN Simulator
import time
import random
class CDNSimulator:
def __init__(self, origins: int = 2, edges: int = 5, cache_size: int = 100):
self.origins = [{"load": 0} for _ in range(origins)]
self.edges = [EdgeCache(cache_size) for _ in range(edges)]
self.latency_matrix = {}
def request(self, user_region: str, url: str, content_size: int) -> dict:
edge_idx = hash(user_region) % len(self.edges)
start = time.time()
cached = self.edges[edge_idx].get(url)
if cached:
latency = random.gauss(5, 2) + (content_size / 100000)
return {"latency_ms": round(latency, 1), "hit": True}
origin_idx = edge_idx % len(self.origins)
self.origins[origin_idx]["load"] += 1
content = b"x" * content_size
self.edges[edge_idx].set(url, content, ttl=3600)
latency = random.gauss(80, 20) + (content_size / 10000)
return {"latency_ms": round(latency, 1), "hit": False}
sim = CDNSimulator()
results = [sim.request("us-east", "/img.png", 50000) for _ in range(100)]
hits = sum(1 for r in results if r["hit"])
avg_latency = sum(r["latency_ms"] for r in results) / len(results)
print(f"Cache hits: {hits}/100")
print(f"Average latency: {avg_latency:.1f}ms")
Expected output:
Cache hits: 85/100
Average latency: 23.4ms
FAQ
What's Next
Congratulations on completing this CDN design! Here's where to go from here:
- Practice daily — Analyze CDN headers on your favorite websites
- Build a project — Set up a CDN for a static site using Cloudflare
- Explore related topics — Anycast routing, edge computing (Workers), image optimization at edge
- Join the community — Share your CDN performance benchmarks and get feedback
Remember: every expert was once a beginner. Keep delivering!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro