Files
atelier/domains/edge/cdn.md
T
Jon Chery 20992883ff docs(P01): complete edge domain phase — v0.4
---ci---
project: atelier
phase: 1
milestone: v0.4
status: complete
phase_role: execution
phase_tag: v0.3.1
requirements:
  covered: [ATELIER-92, ATELIER-93, ATELIER-94, ATELIER-95, ATELIER-96]
  partial: []
---/ci---
2026-08-05 15:51:03 +00:00

227 lines
12 KiB
Markdown

# CDN — Derived Rules
> Derives from `domains/edge/first-principles.md`. Applies P1
> (Proximity is the Design Driver) and P6 (Cache Invalidation is
> Explicit) primarily, with P5 (idempotent cache fill), P8
> (geographic distribution), and P9 (identity at the edge). For the
> edge-cache-vs-origin decision, see the decision matrix below.
> Cross-links `domains/performance/frontend` for generic caching,
> `domains/security/input-validation` for cache poisoning, and
> `domains/observability/metrics` for cache-hit ratio.
## What a CDN Is (P1 Proximity is the Design Driver)
- A content delivery network is a fleet of PoPs (points of presence)
placed near users. The PoP serves cached content; the origin is
the authoritative source. The CDN's whole purpose is P1: compute
(the cache) is placed near the user so the round trip to the origin
does not bound latency. Latency is a correctness constraint at the
edge (C1), not a performance preference.
- The CDN is the canonical edge-cache architecture (Akamai,
Cloudflare, Fastly): PoPs near users, origin shielding, cache-key
normalization, purge APIs. Atelier derives the
placement/invalidation principles, not the vendor config.
- The boundary with `domains/performance/frontend` is per D-061:
performance owns *generic* caching and optimization (cache what is
expensive, stable, read often — `performance/P5 Caching with
Intent`); edge owns the *geographic, partition-aware* placement and
invalidation angle. A CDN is an edge concern because its defining
trait is geographic distribution (P8) and partition-aware
invalidation (P6), not measurement.
## Cache Key Design (P6 Cache Invalidation is Explicit)
- The cache key is the contract between the URL and the cached
representation. A key that varies on the wrong dimensions serves
the wrong content; a key that varies on too many dimensions
collapses the hit ratio. Key design *is* the invalidation
surface: a key that includes a content hash or version segment
makes invalidation explicit; a key that ignores `Vary` headers
serves stale variants.
- Normalize the key: lower-case the host, strip default ports,
sort query parameters, ignore tracking parameters. A
non-normalized key is a cache-poisoning vector (see
`domains/security/input-validation`) and a hit-ratio destroyer
(see `domains/observability/metrics`).
- A cache with no explicit key strategy is a TTL-less cache under
partition (P6 violation): staleness is silent and unbounded.
```http
# Cache key derivation: vary on what changes content, ignore what
# does not. The key is the tuple (host, normalized-path, sorted-
# query, Vary-headers); the cache entry is the representation + TTL.
Cache-Key: example.com /api/v1/products?sort=price&region=us Vary:Accept-Encoding
Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=300
Vary: Accept-Encoding
```
- `max-age` bounds the browser cache; `s-maxage` bounds the CDN
PoP; `stale-while-revalidate` allows serving stale while
refetching. Each is an explicit invalidation strategy (P6).
## TTL vs Explicit Invalidation (P6, C3 Simplicity)
- **TTL-based invalidation** (`max-age`, `s-maxage`): the cache entry
expires after a duration. Simple, no origin contact required to
invalidate, but bounded staleness is the contract — the entry may
be stale up to TTL. Fits content where eventual consistency is
acceptable (asset fingerprints, lists, derived images).
- **Explicit invalidation** (purge, surrogate keys): the operator
signals the cache to drop entries. Tighter staleness bounds, but
requires the origin or operator to know which entries to purge.
Fits content where staleness is a correctness defect (price
updates, availability, breaking news).
- A TTL-less cache with no explicit invalidation is a P6 violation:
stale-forever under partition. Every cache must have one or the
other (or both), and the choice is documented per content type.
## Cache-Hit / Miss / Origin-Fetch (P5, P6)
- **Hit**: the PoP serves from cache. Latency is PoP-local (P1).
- **Miss**: the PoP has no entry; it fetches from the origin (or an
origin-shield PoP). The fetch must be idempotent (P5) — a retried
miss must not corrupt the cache or double-write side effects.
- **Revalidate**: the PoP holds a stale entry and asks the origin
(`If-None-Match`, `If-Modified-Since`); a 304 refreshes the TTL
without re-fetching the body. Revalidation is the bandwidth-economical
middle ground (C8).
```http
# Conditional revalidation the PoP asks the origin "is this still
# current?" The 304 response refreshes the TTL without a body.
GET /api/v1/products HTTP/1.1
Host: example.com
If-None-Match: "etag-7a3f"
HTTP/1.1 304 Not Modified
ETag: "etag-7a3f"
Cache-Control: s-maxage=600
```
- A cache-hit ratio that is not measured is a gate on noise — see
`domains/observability/metrics` for the SLI/SLO discipline that
makes the hit ratio a meaningful signal. A CDN with no hit-ratio
metric is operating blind (P10 analog).
## Origin Shielding (P1, P8, C8 Economy)
- Origin shielding routes all origin fetches through a single
shield PoP (or shield region). The shield absorbs the
thundering-herd: 10 000 PoPs missing the same URL fetch the origin
once, not 10 000 times. This is C8 Economy (origin bandwidth is
bounded) and P1 (the shield is itself a proximity layer for the
origin).
- Shielding is a geographic decision (P8): the shield sits in a
region close to the origin, not close to the user. The shield is
the inner ring of the CDN; the user-facing PoPs are the outer ring.
- A CDN without origin shielding under a stampede will overload the
origin; a shield that is itself partitioned from the origin must
degrade gracefully (P7) — serve stale per `stale-while-revalidate`
rather than 500.
## Purge Strategies (P6, C3 Simplicity)
| Strategy | Granularity | Latency to Invalidate | Cost | Best for |
|----------|-------------|-----------------------|------|----------|
| URL purge | One URL | Seconds | Low (one entry) | Surgical fixes, single-page corrections |
| Soft purge | One URL (mark stale, serve while refetch) | Seconds | Low | High-traffic URLs where a hard purge causes a stampede |
| Surrogate-key purge | A tag set (e.g., `product:123`, `category:shoes`) | Seconds | Medium (key indexing) | Related-content invalidation (a product update purges all its category pages) |
| Wildcard purge | A path prefix or pattern | Seconds to minutes | High (scan) | Site-wide template changes |
| All-cache purge | Everything | Seconds | Very high (origin stampede) | Disaster recovery only; never the steady-state invalidation path |
- Surrogate-key purge (Fastly, Akamai) is the highest-value
strategy: tag cache entries with content keys, then purge by tag.
This is explicit invalidation at scale (P6) without the origin
stampede of an all-cache purge.
- An all-cache purge as the steady-state invalidation path is a P6
violation dressed as a feature — it pushes the origin load back to
100% miss, defeating the CDN's purpose (P1).
## Cache Poisoning Prevention (P9, cross-link security/input-validation)
- A cache poisoned by a crafted request (a URL with a malicious
header that gets cached and served to others) is a correctness
defect (C1) and a security breach (P9 — the edge node is
exploited). Prevent poisoning by:
- Normalizing the cache key (strip untrusted query parameters,
ignore unknown headers, lower-case the host).
- Validating `Vary` against an allow-list; never `Vary: *` on a
shared cache (poisonable via header injection).
- Treating uncacheable responses (`Set-Cookie`,
`Cache-Control: private`) as never-stored.
- See `domains/security/input-validation` for the general
input-validation discipline the cache key must follow. The cache
key is a validation surface; a non-validated key is an attack
surface.
## Multi-CDN Routing (P8 Geographic Distribution)
- A multi-CDN strategy routes each request to the best PoP across
providers (Akamai + Cloudflare + Fastly). Routing is
location-aware (P8): latency, cost, and availability vary by
region and provider. The DNS layer (or a client-side router)
selects the CDN per request.
- Multi-CDN is a P8 decision, not a vendor-management decision:
geographic distribution is the first-class constraint. A
single-CDN deployment routes everything to one provider's PoPs;
a multi-CDN deployment routes by region, latency, and cost.
- Invalidation across multiple CDNs is harder (P6): each provider
has its own purge API and surrogate-key scheme. A multi-CDN purge
must fan out to all providers; a purge that reaches only one CDN
leaves the others stale. Track purge completion per provider —
see `domains/observability/metrics` for the per-CDN hit-ratio and
purge-latency signals.
```http
# A CDN config example: cache-control headers + a purge rule.
# Origin response: declare the cache contract (P6).
HTTP/1.1 200 OK
Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=300
Surrogate-Key: product:123 category:shoes
ETag: "etag-7a3f"
Vary: Accept-Encoding
# Purge rule (Fastly-style surrogate-key): when product 123
# updates, purge every cache entry tagged product:123 OR
# category:shoes. Explicit, bounded, no origin stampede (P6).
POST /service/svc1/purge
Surrogate-Key: product:123 category:shoes
# Returns: {"status": "ok", "id": "purge-abc"} — poll the purge
# status to confirm completion across all PoPs (P8, P10).
```
## Edge-Cache vs Origin — Decision Matrix (D-069)
| Strategy | When | Latency | Origin Load | Correctness Risk |
|----------|------|---------|-------------|------------------|
| Serve from PoP (cache hit) | The PoP holds a fresh entry (within TTL or revalidated) | Lowest (PoP-local, P1) | None | Low — bounded by TTL staleness (P6) |
| Serve stale while revalidate | The PoP holds a stale entry and `stale-while-revalidate` is set | Low (stale served immediately, refetch in background) | Background refetch (1 per entry) | Medium — stale served up to the revalidate window; acceptable for eventually-consistent content |
| Fetch fresh from origin (miss) | The PoP has no entry, or the content is non-cacheable | High (origin round trip) | Full fetch per miss | Low — fresh by construction; the miss is the correctness floor |
| Origin-shield fetch | Multiple PoPs miss the same URL; the shield collapses the herd | Medium (PoP → shield → origin) | Bounded to one origin fetch per shield (C8) | Low — shield is the inner ring; staleness bounded by shield TTL |
| Purge and serve fresh | Explicit invalidation received (surrogate-key or URL purge) | Medium (purge propagates, then fresh fetch) | Full fetch post-purge | Lowest — explicit invalidation is the tightest staleness bound (P6) |
| Serve from origin directly (bypass cache) | Content is non-cacheable (personalized, real-time) | Highest (every request hits origin) | Full fetch per request | Lowest for correctness, highest for origin load — use sparingly |
- The default is **serve from PoP** when fresh, **fetch fresh from
origin** on miss with **origin-shield** to bound origin load, and
**purge and serve fresh** when explicit invalidation is required.
Bypass-the-cache is for non-cacheable content only — bypassing for
cacheable content is a P1 violation (you have defeated the CDN).
- The correctness risk column is bounded by the invalidation
strategy (P6): every row except "bypass" carries staleness risk
that is bounded by TTL or explicit purge. A row with no
invalidation strategy is a P6 violation.
## What Violates CDN Discipline
| Violation | Principle |
|-----------|-----------|
| TTL-less edge cache under partition (stale-forever, no explicit invalidation) | P6 Cache Invalidation is Explicit |
| Cache key that varies on untrusted query parameters (poisonable) | P6, P9 (`domains/security/input-validation`) |
| All-cache purge as the steady-state invalidation path (origin stampede) | P6, C8 Economy |
| Non-normalized cache key (case-sensitive host, unsorted query) | P6, `domains/security/input-validation` |
| Bypass-the-cache for cacheable content | P1 Proximity is the Design Driver (defeats the CDN) |
| Multi-CDN with no per-CDN purge completion tracking | P8, P10 (stale cache invisible to the operator) |
| Origin fetch that is not idempotent under retry | P5 Edge Operations are Idempotent |
| Cache-hit ratio not measured | P10, `domains/observability/metrics` |
| Single-CDN deployed where geographic distribution requires multi-CDN | P8 Geographic Distribution |
| Shield PoP that 500s instead of serving stale under partition | P7 Partial Degradation is Engineered |