CDN and edge cache architecture: cache strategies, breakdown/avalanche/penetration, Cache-Control practice

puml.online

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.

CDN edge cache hierarchy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@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.

HTTP cache strategy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@startuml
title "HTTP Cache Decision Tree"

start

:receive HTTP response;

if (response Cache-Control: no-store)
:don't cache anywhere;
stop
elseif (Cache-Control: no-cache)
:cache but always revalidate;
:server ETag compare;
if (304 Not Modified)
:use cache;
else
:re-download;
end
elseif (Cache-Control: max-age=N)
:cache N seconds;
if (re-request within N)
:use cache directly;
else
:revalidate;
end
elseif (Cache-Control: public)
:all intermediate caches allowed;
elseif (Cache-Control: private)
:browser only;
:CDN cannot cache;
elseif (Expires past)
:compare expiration time;
end

stop

@enduml

Cache-Control key values:

1
2
3
4
5
6
7
8
9
10
Cache-Control: public, max-age=31536000, immutable
# public cache 1 year, never changes (hashed filename)
Cache-Control: public, max-age=3600
# public cache 1 hour
Cache-Control: private, max-age=300
# browser only 5 min
Cache-Control: no-cache
# cache but always revalidate
Cache-Control: no-store
# never cache

s-maxage vs max-age:

  • max-age — browser cache time
  • s-maxage — CDN cache time (overrides max-age)

ETag + Last-Modified revalidation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@startuml
title "ETag / Last-Modified Validation"

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

@enduml

ETag precedence:

  • strong ETag — content hash ("abc123") — byte-level equality
  • weak ETagW/"abc123" — semantic equality (HTML reformat counts as same)

Three cache disasters

Breakdown (single key expires)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@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
def get_hot_key(key):
val = redis.get(key)
if val is None:
# only one request goes to DB
with redis.lock(f"lock:{key}", timeout=10):
val = redis.get(key) # double check
if val is None:
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

Avalanche (mass keys expire together)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@startuml
title "Cache Avalanche (mass keys expire together)"

participant "User" as User
participant "Redis" as Cache
participant "DB" as DB

note over Cache
2026-01-15 00:00:00
all keys expire together
(same set time)
end note

User -> Cache : ① GET key_1
Cache --> User : miss
User -> DB : ② SELECT key_1

User -> Cache : ③ GET key_2
Cache --> User : miss
User -> DB : ④ SELECT key_2

note over DB
1M keys expire together
all hit DB
DB dies
end note

DB --> User : ⑤ timeout

@enduml

Fix:

  1. randomize expiration: expire = base + random(0, 300) avoid same time
  2. multi-tier cache: Redis miss → local cache fallback
  3. circuit breaker: when DB slow, return degraded data

Penetration (query non-existent keys)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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)

def get_user(user_id):
if not bloom.contains(user_id):
return None # definitely doesn't exist
val = redis.get(f"user:{user_id}")
if val is None:
val = db.query(user_id)
if val:
redis.setex(f"user:{user_id}", 3600, val)
return val

Fix — null caching: redis.setex(f"user:{nonexistent_id}", 60, "null") — null values short-cached, defense against penetration attack.

CDN cache + Redis coordination

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@startuml
title "CDN + Redis Multi-Tier Cache"

actor "User" as User
participant "CDN Edge" as CDN
participant "Redis Cluster" as Redis
participant "Origin Server" as Origin
database "DB" as DB

User -> CDN : ① GET /api/products
CDN -> CDN : ② cache hit/miss?

alt Edge hit (10s)
CDN --> User : ③ 200 (Edge cache)
else Edge miss
CDN -> Redis : ④ GET product (region cache)

alt Redis hit (5min)
Redis --> CDN : ⑤ 200 (Redis data)
CDN --> User : ⑥ 200 + Cache-Tag: edge,redis
else Redis miss
CDN -> Origin : ⑦ GET /api/products
Origin -> DB : ⑧ SELECT
DB --> Origin : ⑨ products
Origin --> CDN : ⑩ 200 products
CDN -> CDN : ⑪ cache to Edge (10s TTL)
CDN -> Redis : ⑫ SETEX products 5min
CDN --> User : ⑬ 200
end
end

@enduml

Multi-tier cache key:

  • Edge (10s) — very short, absorbs burst
  • Redis (5min) — medium, fallback on edge miss
  • Origin — long cache (1h+) or no-cache

Edge functions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@startuml
title "Edge Function Personalization"

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
  • header rewrite — inject user ID etc
  • rate limiting — return 429 at edge

Cloudflare/CloudFront/Vercel comparison

Dimension Cloudflare CloudFront Vercel Edge
Node count 300+ 600+ 100+
Edge functions Workers (V8) Lambda@Edge Edge Functions
Price free tier sufficient per request + GB per request + GB
DDoS protection built-in free WAF plan needed basic
Best for all scenarios AWS ecosystem Next.js / Vercel

Selection:

  • personal / SME → Cloudflare free + Workers
  • AWS heavy → CloudFront + Lambda@Edge
  • Next.js fullstack → Vercel Edge

Cloudflare Workers example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// workers/cache-headers.js
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
const response = await fetch(request);
const newResponse = new Response(response.body, response);

// add cache headers
newResponse.headers.set('Cache-Control', 'public, max-age=300, s-maxage=3600');

// add security headers
newResponse.headers.set('X-Content-Type-Options', 'nosniff');
newResponse.headers.set('X-Frame-Options', 'DENY');
newResponse.headers.set('Referrer-Policy', 'no-referrer');

return newResponse;
}

CloudFront Lambda@Edge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// CloudFront → Lambda@Edge → Origin Response
// viewer-response.js
exports.handler = async (event) => {
const response = event.Records[0].cf.response;

// set cache time
response.headers['cache-control'] = [{
key: 'Cache-Control',
value: 'public, max-age=86400, s-maxage=604800'
}];

// CSP header
response.headers['content-security-policy'] = [{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-inline'"
}];

return response;
};

Field foot-guns

  • 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

CDN selection?
├─ Personal / SME → Cloudflare free
├─ AWS ecosystem → CloudFront
├─ Vercel deployment → Vercel Edge
└─ Self-hosted → self-built varnish/nginx proxy_cache

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
  • Author: puml.online
  • Created at : 2026-07-30 18:05:00
  • Updated at : 2026-08-14 21:34:29
  • Link: https://puml.online/blog/plantuml-cdn-edge-cache-en/
  • License: This work is licensed under CC BY-NC-SA 4.0.