Troubleshooting · CDN edge failures
Streamwake reliability agents

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.

Protocol: CDN
Format: cache-control + edge logs
Streamwake probes: cache state · edge routing · origin fallback · multi-CDN.

Book a technical demo for CDN edge failures

Lead magnet
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.

Probes

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.

Cache state
cache.availability + cache.freshness
edge · cache
  • HEAD against a known probe URL reads X-Cache, Age, Via, X-Served-By.
  • Freshness vs declared max-age: Age must be below the cache-control budget.
  • Stale-warning header (Warning: 110) should never appear on the live edge path.
Edge routing & purge
edge.purge_acknowledged + edge.geo_drift
routing
  • 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-By matches the expected POP for the probe source's region.
Origin fallback
origin.failover_timing + origin.reachable
origin
  • 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.
Header contract
headers.cache_control_inheritance + headers.vary_collapse
headers
  • Cache-Control on the wire matches what the path's contract specifies (not what a debug rule globbed onto it).
  • Vary collapse: distinct cache keys per URL should stay under 4 within the probe window.
  • Age is preserved across redirects; if it resets, the probe flags it.
Multi-CDN failover
multicdn.winner_RTT + multicdn.health_score
multi-CDN
  • 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

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.

MISS trace (cold cache, edge reaches origin)
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-1e2f3a4b5c6d
Stale-on-revalidate trace (HIT, Age > max-age)
GET 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
Probes → response headers
Quick map from probe verdict to the header you should pull.
  • cache.availability X-Cache / X-Cache-Hits
  • cache.freshness Age vs Cache-Control: max-age
  • edge.geo_drift X-Served-By / Via
  • headers.cache_control_inheritance Cache-Control on 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.

Failure modes

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.

01CDN returns 200 with the wrong body — stale-on-revalidate
failure mode
Symptom

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.

Root cause

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.

Fix

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.

02Cache-Control: max-age=60 inherited from a debug response
failure mode
Symptom

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.

Root cause

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.

Fix

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.

03Soft purge returns 200 but the next GET is still HIT
failure mode
Symptom

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.

Root cause

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.

Fix

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.

04Vary: Accept-Encoding fragmented into thousands of cache keys
failure mode
Symptom

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.

Root cause

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.

Fix

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.

05Edge POP mis-routes request to a sibling region after a deploy
failure mode
Symptom

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.

Root cause

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.

Fix

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.

06X-Cache: MISS on every request despite a warm window
failure mode
Symptom

Cache hit rate flatlines near zero. Origin logs spike. Player first-segment latency balloons because every segment fetch is a cold miss.

Root cause

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.

Fix

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.

07Origin shield saturates and edge starts reaching origin directly
failure mode
Symptom

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.

Root cause

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.

Fix

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.

08Multi-CDN race — both providers start serving, but only one is healthy
failure mode
Symptom

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.

Root cause

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.

Fix

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.

09Age resets to 0 on redirects the CDN didn't strip
failure mode
Symptom

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.

Root cause

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.

Fix

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.

10Purge API returns 202 Accepted and the operator thinks it's done
failure mode
Symptom

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.

Root cause

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.

Fix

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

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.

POST /api/v1/streams
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
  }'
GET /api/v1/agents?stream_id=…
curl https://streamwake.polsia.io/api/v1/agents?stream_id=<id> \
  -b "better-auth.session_token=<your-session-cookie>"
Agent timeline response (trimmed)
{
  "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"
    }
  ]
}
Next step

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.

Need Streamwake on one of your incidents?
Would you like Streamwake to analyze one of your historical incidents and show where AI could reduce investigation time? (Filed under: CDN edge failures.)
Incident analysis
  • 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.