CDN is more than “make images load fast” — it’s edge compute + cache strategy + security combined. This is CDN hierarchy, cache strategies, Cache-Control header deep-dive, three cache disasters and fixes, Cloudflare/CloudFront/Vercel selection.
@startuml skinparam componentStyle rectangle skinparam defaultTextAlignment center
title "CDN Edge Cache Hierarchy"
actor "User" as User
rectangle "User side" { component "Browser Cache" as BC }
rectangle "Edge nodes" { component "Edge PoP\n(Cloudflare/CloudFront)" as Edge }
rectangle "Regional center" { component "Regional Cache\n(mid-tier)" as Regional }
rectangle "Origin" { component "Origin Shield" as Shield component "Origin Server" as Origin }
User -> BC : ① first request BC --> User : miss
User -> Edge : ② second request Edge --> User : hit / miss Edge -> Regional : ③ miss Regional --> Edge : ④ hit / miss Regional -> Shield : ⑤ miss Shield --> Regional : ⑥ hit / miss Shield -> Origin : ⑦ miss Origin --> Shield : ⑧ respond
note right of BC browser cache - strong cache: Cache-Control - revalidate: ETag/Last-Modified end note
note right of Edge Edge PoP - Cloudflare 300+ nodes - CloudFront 600+ nodes - 0-50ms from user end note
note right of Shield Origin Shield - dedup identical requests - reduce origin load end note
@enduml
Hierarchy meaning:
Browser Cache — user local, milliseconds
Edge PoP — city-level, 10-50ms
Regional — region-level, 50-100ms
Origin — source, 100-500ms
The deeper the tier, the narrower but closer to user.
actor "User" as User participant "Server" as Server
User -> Server : ① GET /article.html Server --> User : ② 200 + ETag: "abc123" + Last-Modified: Mon, 01 Jan 2026
note over User : browser caches ETag and Last-Modified
User -> Server : ③ GET /article.html\nIf-None-Match: "abc123"\nIf-Modified-Since: Mon, 01 Jan 2026
alt content unchanged Server --> User : ④ 304 Not Modified note over User : use browser cache else content changed Server --> User : ⑤ 200 + new content + new ETag end
@startuml title "Cache Breakdown (single key expires)"
participant "User1" as U1 participant "User2" as U2 participant "User3" as U3 participant "Redis" as Cache participant "DB" as DB
U1 -> Cache : ① GET hot_key Cache --> U1 : miss (key expired)
U2 -> Cache : ② GET hot_key Cache --> U2 : miss (still expired)
U3 -> Cache : ③ GET hot_key Cache --> U3 : miss (still expired)
note over DB three concurrent requests penetrate to DB same hot_key QPS spike 100x end note
U1 -> DB : ④ SELECT hot_key U2 -> DB : ④ SELECT hot_key U3 -> DB : ④ SELECT hot_key
DB --> U1 : ⑤ slow (CPU 100%) DB --> U2 : ⑤ slow DB --> U3 : ⑤ slow
@enduml
Fix — mutex lock:
1 2 3 4 5 6 7 8 9 10
defget_hot_key(key): val = redis.get(key) if val isNone: # only one request goes to DB with redis.lock(f"lock:{key}", timeout=10): val = redis.get(key) # double check if val isNone: val = db.query(key) redis.setex(key, 3600, val) return val
Fix — never expire:
background async refresh
business logic uses logical expiration, cache always present
@startuml title "Cache Penetration (query non-existent keys)"
participant "Attacker" as A participant "Cache" as C participant "DB" as DB
A -> C : ① GET user_id=-1 C --> A : miss (doesn't exist) A -> DB : ② SELECT WHERE id=-1 DB --> A : ③ null
A -> C : ④ GET user_id=-2 C --> A : miss A -> DB : ⑤ SELECT WHERE id=-2 DB --> A : ⑥ null
note over DB attacker loops non-existent keys bypasses cache, hits DB DB CPU 100% end note
@enduml
Fix — Bloom filter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
# load all existing user_id into bloom filter on boot bloom = BloomFilter(capacity=10_000_000, error_rate=0.001) for user in db.query("SELECT id FROM users"): bloom.add(user.id)
defget_user(user_id): ifnot bloom.contains(user_id): returnNone# definitely doesn't exist val = redis.get(f"user:{user_id}") if val isNone: val = db.query(user_id) if val: redis.setex(f"user:{user_id}", 3600, val) return val
actor "User" as User participant "CDN Edge\nwith Worker" as Edge participant "Origin" as Origin
User -> Edge : ① GET /api/recommendations Edge -> Edge : ② Worker reads cookie / IP Edge -> Edge : ③ decide region (Beijing/Shanghai) Edge -> Edge : ④ cache key = region + user_segment Edge -> Edge : ⑤ cache hit?
alt Hit Edge --> User : ⑥ 200 (cache) else Miss Edge -> Origin : ⑦ GET /recommendations?region=... Origin --> Edge : ⑧ 200 recommendations Edge --> User : ⑨ 200 + cache end
@enduml
What edge functions can do:
A/B testing — decide variant at edge
region routing — CN user → CN origin
JWT verification — reject invalid tokens without roundtrip
CDN caches user-specific content — user pages get shared cache. Set Cache-Control: private or add Vary: Cookie.
Cache TTL too long — content changes, users don’t see updates. HTML uses max-age=0 + ETag, JS/CSS uses immutable + hash filename.
CDN caches 5xx errors — origin down, CDN returns 5xx. Configure Cache-Control: no-store or 5xx not cached.
Cross-origin CORS cache conflict — browser caches response with CORS, next request without CORS header. Vary: Origin makes CDN cache per-origin.
Origin traffic cost — CDN miss goes to origin, origin bandwidth cost. CloudFront origin shield reduces origin hits.
Edge function timeout — Lambda@Edge 5s timeout. Slow logic not at edge.
Edge function stateless — cannot rely on local memory. Use KV / Durable Objects for state.
Cold start latency — edge function first call slow. Pre-warm + keep-alive.
Cache key design wrong — different users get same cache. Key includes user_id or session_id.
Decision tree
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
What to cache? ├─ Static assets (JS/CSS/images) → CDN long TTL + hash filename ├─ HTML → CDN short TTL + ETag ├─ API GET → Redis + CDN fallback ├─ API POST/PUT → no cache └─ User-specific → Cache-Control: private, no CDN
Cache strategy? ├─ Fully static → immutable, 1 year ├─ Semi-static (article) → max-age=300, revalidate ├─ Dynamic API → max-age=0, always revalidate └─ User-specific → private, no CDN
Minimum start: Cloudflare free + Cache-Control: public, max-age=3600. Advanced: add edge functions for A/B routing, region routing, JWT verification.
Remember: CDN is performance optimization, not silver bullet. Business logic must not depend on CDN cache (cache may fail), critical data goes to strong-consistency DB. Cache strategy must monitor hit rate — hit rate < 80% means strategy has problems.
Title: CDN and edge cache architecture: cache strategies, breakdown/avalanche/penetration, Cache-Control practice