Recover CDN edge failures
without guessing.
A working guide to the failure patterns Streamwake catches on real CDN edges — from cold misses and stale-on-revalidate through soft-purge gaps, edge POP mis-routing, origin shield saturation, and multi-CDN failover — written so an on-call engineer can read it next to a recent edge-log capture and act on it.
Book a technical demo for CDN edge failures
Read the postmortem — then bring your own incident to Streamwake.
Two ways to engage on this exact failure pattern: book a 30-minute technical demo where we walk through the probe cascade on your source, or hand us an archived incident and watch the agent diagnose it end-to-end.
Both routes land on the scoping intake form — no SDR gate.
What Streamwake checks
Five families of probes, each with a small, deterministic pass/fail verdict that feeds the timeline. Every check has a name — that's the name you'll see on the agents feed.
- HEAD against a known probe URL reads
X-Cache,Age,Via,X-Served-By. - Freshness vs declared max-age:
Agemust be below the cache-control budget. - Stale-warning header (
Warning: 110) should never appear on the live edge path.
- After a soft-purge, the next GET from at least 3 geo probes reads
X-Cache: MISS. - Purge-controller latency budget: fan-out ack within 30 seconds.
X-Served-Bymatches the expected POP for the probe source's region.
- Failover budget: how long before the edge gives up the cache and reaches origin.
- Origin shield's bypass rate: spike without a shield fault means the reroute rule is mis-configured.
- Origin reachable from the edge in <500ms under normal load.
Cache-Controlon the wire matches what the path's contract specifies (not what a debug rule globbed onto it).Varycollapse: distinct cache keys per URL should stay under 4 within the probe window.Ageis preserved across redirects; if it resets, the probe flags it.
- Routing winner per probe interval is the provider with the lowest RTT.
- Health score feeds DNS weights directly; convergence within 30 seconds.
- Unhealthy providers drop to weight 0 until three consecutive intervals report health > 0.95.
Anatomy of a CDN response
Two traces — a clean MISS and a stale-on-revalidate HIT — annotated with the probe that reads from each header. When an alert fires, the table below names the header to grep for in your CDN's access logs.
GET https://cdn.example.com/live/event/manifest.m3u8
HTTP/2 200
cache-control: public, max-age=2
content-type: application/vnd.apple.mpegurl
age: 0
date: Thu, 06 Aug 2026 10:42:13 GMT
via: 1.1 edge-pop-lax03
x-cache: MISS
x-cache-hits: 0
x-served-by: cache-lax6123
x-request-id: 9f3c1d2e-4b5a-4d6e-b0c7-1e2f3a4b5c6dGET https://cdn.example.com/live/event/manifest.m3u8
HTTP/2 200
cache-control: public, max-age=2, stale-if-error=86400
content-type: application/vnd.apple.mpegurl
age: 47
date: Thu, 06 Aug 2026 10:43:00 GMT
last-modified: Thu, 06 Aug 2026 10:41:48 GMT
via: 1.1 edge-pop-fra02
warning: 110 - "Response is Stale"
x-cache: HIT
x-cache-hits: 28
x-served-by: cache-fra9921
x-request-id: 7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d- cache.availability →
X-Cache/X-Cache-Hits - cache.freshness →
AgevsCache-Control: max-age - edge.geo_drift →
X-Served-By/Via - headers.cache_control_inheritance →
Cache-Controlon the wire - headers.vary_collapse →
Vary+ distinct cache key count - origin.reachable →
X-Cache: MISS+ origin latency
The MISS trace is the cold-cache case — Age: 0, X-Cache: MISS, and the edge POP identity in X-Served-By. It's what the cache.availability probe treats as the green baseline.
The stale-on-revalidate trace is the bug a manifest cache hides for hours: Age: 47 against a declared max-age=2, Warning: 110 signaling the body is stale, and stale-if-error telling the edge to keep serving it. The cache.freshness probe flags this exact pattern.
In CDN access logs, the header contracts above are the only fields that distinguish a cache miss from a cache-freshness miss from a stale-on-revalidate silently serving old bytes. The map on the left is the canonical lookup for which header to grep on.
Ten ways a CDN edge fails
Each row: symptom the agent reports → the underlying cause → a fix that holds under the next probe cycle. The named probes are what you'd grep for in the agents feed.
Edge returns HTTP 200 with a manifest whose MEDIA-SEQUENCE / PDT is from minutes ago. Player keeps the same dead media sequence even though new segments have landed.
Stale-while-revalidate / stale-if-error is wider than the live window budget. Edge served the cached body because origin failed revalidation after a brief blip, and the cache-control directive instructed the edge to keep serving the stale body for hours.
Tighten stale-if-error to under one live window; pin Cache-Control: no-cache, must-revalidate on master playlists; have the agent's cache.freshness probe fail when age > declared max-age.
A path that should be no-store (token URLs, signed segments) is being cached for 60 seconds at the edge — the next viewer hits a 200 with the prior user's signed URL.
A debug or staging endpoint set Cache-Control: max-age=60 and the CDN's ruleset globbed it onto production URLs. The edge applies the header because the rule matched the path, not the contract the origin intended.
Make Cache-Control contracts explicit per content class — origins should always emit no-store on signed URLs and max-age only on unsigned assets; the headers.cache_control_inheritance probe should flag any origin Cache-Control leaked onto a path the manifest treats as private.
Operator fires a soft-purge, the purge API returns 200 OK, and a follow-up curl against the same URL still returns X-Cache: HIT with the old body. Viewers see stale content after a "successful" purge.
The purge API is asynchronous across edge POPs — 200 means the purge was accepted, not propagated. Some POPs received the purge instruction late, or the purge was scoped to a tag the edge didn't fully honor.
Probe edge.purge_acknowledged for at least 30 seconds after a purge; spread the verification curl across multiple geo POPs; require the operator to wait for fan-out ack before considering the purge done.
Cache hit rate drops to single digits on the manifest path despite a clearly warm window. CDN metrics show millions of cache keys for the same URL.
Vary: Accept-Encoding is being honored at edge AND at a parent POP, producing keys the parent POP treats as distinct. A single Accept-Language variant doubles the population, a user-agent tweak triples it.
Collapse Vary keys at the edge before fan-out; only forward Vary to a parent POP if the manifest genuinely varies on that header. headers.vary_collapse should report distinct keys < 4 within minutes of probe start.
EMEA viewers start seeing X-Served-By: cache-jfk8842 — a North America POP. Latency spikes, manifest deliverability drops, and EMEA's health_score degrades within minutes.
GeoDNS / anycast routing table was updated by a deploy that didn't fully sync the POP weights. The new table picks the wrong POP for a region's IP block, and anycast falls back to a less-loaded neighbor.
Pin geo_routing tests to expected POPs; flag any X-Served-By outside the expected region; the edge.geo_drift probe should run as a synthetic check from at least three geo probes against the same URL.
Cache hit rate flatlines near zero. Origin logs spike. Player first-segment latency balloons because every segment fetch is a cold miss.
A query-string cache buster was added at the player layer (a timestamp or a session key), and the CDN is treating every /manifest.m3u8?... URL as a distinct cache key. The cache is empty because nothing maps to it twice.
Strip the busting query string at the edge; have the CDN's cache key only on the canonical path and the variant segments; the cache.availability probe should report a non-zero hit rate within the first probe interval.
Origin connections spike. Origin CPU saturates. Edge metrics show requests bypassing the origin shield and reaching origin directly — yet the shield isn't reporting a fault.
Origin shield reroute rule was configured to bypass on shield latency > 50ms. Under load, the shield crossed 50ms and the bypass rule streered traffic straight to origin, escalating the load.
Tune the reroute threshold against real shield capacity (not a static ms limit); the origin.failover_timing probe must fire when bypass rate spikes without a corresponding shield fault.
Multi-CDN routing pulls traffic to two providers; one is healthy, one is degraded (5xx > 5% for 5 minutes). DNS still weights 50/50, so half the requests go to the unhealthy provider.
DNS weight drift — the multi-CDN controller is slow to converge on the unhealthy provider's health score. While it converges, both providers serve traffic; the degraded provider drags aggregate health down.
Have the multicdn.health_score probe feed DNS weights directly; require controllers to converge within 30 seconds; pin the unhealthy provider to 0 weight until three consecutive probe intervals report health > 0.95.
Inner response header Age: 0 reads cleanly, but the parent metrics show the same URL as a fresh miss every refresh. Player perceives cache misses despite manifest being served from origin only seconds ago.
A redirect chain (origin → parent POP → edge POP) does not propagate the Age header — the inner Age is reset at each hop. The header contract in place ignores Age on the inner response and computes its own age.
Preserve Age across hops; document the Age behavior so monitors can distinguish "edge cache miss" from "redirect reset Age"; if you can't preserve it, surface a synthetic X-Cache-Age header that operators can grep.
Operator runs a purge, the response shows 202 Accepted, and PagerDuty is closed because "purge succeeded". 30 minutes later, viewers report stale content; the edge has not yet purged.
The purge API uses 202 for asynchronous acceptance, but the on-call runbook treats 2xx as success. The actual purge runs asynchronously across the edge fleet; the runbook never waits for fan-out ack.
Treat 202 as "accepted, not done"; have an automated fan-out verification ping 30 seconds post-purge; the edge.purge_acknowledged probe should be part of the on-call runbook before closing the ticket.
Diagnose with Streamwake
Register the CDN probe against POST /api/v1/streams, then read the agent timeline back through GET /api/v1/agents. The probe verdicts in the timeline are exactly the rows above.
The curl below registers a CDN edge URL and asks for a 60-second probe cadence. CDN probes are run from at least three geo probes against the same URL — so the edge.geo_drift and edge.purge_acknowledged verdicts read real cross-region data. The cookie is the same better-auth.session_token that gates every /api/v1/* call — see the auth guide for how to mint one.
Once the probe is registered, the agents endpoint returns the per-probe verdicts below. The order of checks mirrors the probe families in the section above — cache.availability first, edge routing next, origin fallback, then header contract and multi-CDN.
curl -X POST https://streamwake.polsia.io/api/v1/streams \
-H "content-type: application/json" \
-b "better-auth.session_token=<your-session-cookie>" \
-d '{
"sourceUrl": "https://cdn.example.com/live/event/manifest.m3u8",
"protocol": "CDN",
"probeIntervalSeconds": 60
}'curl https://streamwake.polsia.io/api/v1/agents?stream_id=<id> \
-b "better-auth.session_token=<your-session-cookie>"{
"stream_id": "cklivecdnevent789",
"source": "https://cdn.example.com/live/event/manifest.m3u8",
"protocol": "CDN",
"checks": [
{
"probe": "cache.availability",
"result": "pass",
"detail": "origin 200, x-cache=MISS expected on cold probe"
},
{
"probe": "cache.freshness",
"result": "fail",
"age_seconds": 47,
"detail": "served HIT with age=47 > declared max-age=2 — stale-on-revalidate, X-Cache: HIT but body is stale window"
},
{
"probe": "edge.purge_acknowledged",
"result": "pass",
"latency_ms": 318,
"detail": "soft purge returned 200, next GET read X-Cache: MISS"
},
{
"probe": "edge.geo_drift",
"result": "fail",
"detail": "X-Served-By: cache-jfk8842 in EMEA session — edge POP mis-routed after recent deploy"
},
{
"probe": "origin.failover_timing",
"result": "pass",
"failover_ms": 142,
"detail": "origin shield absent; edge reached origin in 142ms — under 500ms threshold"
},
{
"probe": "origin.reachable",
"result": "pass",
"latency_ms": 96
},
{
"probe": "headers.cache_control_inheritance",
"result": "fail",
"detail": "edge-applied Cache-Control: max-age=60 inherited from debug /_debug endpoint"
},
{
"probe": "headers.vary_collapse",
"result": "fail",
"detail": "Vary: Accept-Encoding produced 4128 distinct cache keys on the manifest URL"
},
{
"probe": "multicdn.winner_RTT",
"result": "pass",
"winner": "cdn-a",
"rtt_ms": 38,
"detail": "cdn-a 38ms, cdn-b 71ms — cdn-a retained routing"
},
{
"probe": "multicdn.health_score",
"result": "fail",
"detail": "cdn-b health 0.62 over 5 min — degraded, but DNS still weighted 50/50"
}
]
}Want Streamwake to catch this on its own?
Sign up, register a CDN probe, and the same probes that produced the timeline above run on every refresh — and surface in a Slack channel, a webhook, or the streams dashboard.
- Pick a recent on-call incident — manifest stall, edge miss, player-side stall, or peer congestion.
- We replay it through the same reliability-agent probe cascade used on the postmortem above.
- You walk away with a written what-could-have-been-Automated readout, not a sales deck.