Recover failing HLS sessions
without guessing.
A working guide to the failure patterns Streamwake catches on real HLS sessions — from master playlist fetch through EXT-X tag continuity, CMAF packaging, and low-latency edge cases — written so an on-call engineer can read it next to a recent chunklist capture and act on it.
What Streamwake checks
Five families of probes, each with a small, deterministic pass / fail / warn verdict that feeds the timeline. Every check has a name — that's the name you'll see on the agents feed.
- Master playlist returns HTTP 200 within the probe budget (default 5s).
- Content-Type reads as
application/vnd.apple.mpegurl— not octet-stream. #EXTM3Uon line 1;EXT-X-VERSIONmatches features used; no BOM.
- Every
STREAM-INFvariant URI resolves to a coherent media playlist. - MEDIA-SEQUENCE advances by the EXTINF stride; PDT is monotonic.
- Declared vs actual segment duration drift across the trailing 6 segments.
- SHA-256 over a leading segment from the primary CDN vs the same URI on a backup CDN — headers match, bytes do not.
- Catches stale cache tiers that came up after a rotation.
- Flags a redirect chain that changes byte order without changing the URI.
- TTFB p95 against a phased baseline — flags egress saturation early.
- In-flight fetches on the outbound pool vs fanout pattern; join-storm signature.
- Auto-throttles probe fan-out when the pool is near its ceiling.
- #EXT-X-DISCONTINUITY only at genuine PTS / codec / format transitions, paired with the SCTE-35 cue.
- MEDIA-SEQUENCE monotonic — catches packager restarts without tags.
- PART-HOLD-BACK matches the delta cursor; CAN-PRELOAD-BEFORE aligned with PART-INF timing for low-latency CMAF.
A master + variant playlist pair
A master with three video variants + one audio rendition group, and a 720p variant with EXT-X-MEDIA-SEQUENCE, EXTINF, and EXT-X-PROGRAM-DATE-TIME. The annotations below name the probe that reads each element.
The master declares the variant ladder and the audio rendition group that every variant references via AUDIO. Each EXT-X-STREAM-INF line is what variant.consistency reads to walk the ladder.
Inside the variant, EXT-X-MEDIA-SEQUENCE is the live-edge anchor; EXT-X-PROGRAM-DATE-TIME is the wall-clock projection the segment.tail_window probe asserts monotonicity against. The EXTINF value is what the duration-drift probe compares against the actual segment bytes.
- playlist.reachable → root
#EXTM3U - variant.consistency →
#EXT-X-STREAM-INFsibling rows - segment.head_window → first
#EXTINFsegment URI - segment.tail_window →
#EXT-X-MEDIA-SEQUENCEresolved against#EXT-X-PROGRAM-DATE-TIME - segment.hash_drift → SHA-256 of the leading
.m4sacross CDNs - llhls.part_holdback_drift →
#EXT-X-PART-HOLD-BACKvs delta cursor
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-aac",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,URI="audio-aac/playlist.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=628000,CODECS="mp4a.40.2,avc1.640028",RESOLUTION=640x360,AUDIO="audio-aac"
video-360p/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2428000,CODECS="mp4a.40.2,avc1.640028",RESOLUTION=1280x720,AUDIO="audio-aac"
video-720p/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=4828000,CODECS="mp4a.40.2,avc1.640028",RESOLUTION=1920x1080,AUDIO="audio-aac"
video-1080p/playlist.m3u8#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:1470
#EXT-X-MAP:URI="video-720p/init.mp4"
#EXT-X-PROGRAM-DATE-TIME:2026-08-09T10:42:13.000Z
#EXTINF:6.0,
video-720p/seg-1470.m4s
#EXT-X-PROGRAM-DATE-TIME:2026-08-09T10:42:19.000Z
#EXTINF:6.0,
video-720p/seg-1471.m4s
#EXT-X-PROGRAM-DATE-TIME:2026-08-09T10:42:25.000Z
#EXTINF:6.0,
video-720p/seg-1472.m4s
#EXT-X-PROGRAM-DATE-TIME:2026-08-09T10:42:31.000Z
#EXTINF:6.0,
video-720p/seg-1473.m4s
#EXT-X-PROGRAM-DATE-TIME:2026-08-09T10:42:37.000Z
#EXTINF:6.0,
video-720p/seg-1474.m4s
#EXT-X-PROGRAM-DATE-TIME:2026-08-09T10:42:43.000Z
#EXTINF:6.0,
video-720p/seg-1475.m4sTen ways an HLS session fails
Each row: symptom the agent reports → underlying cause → a fix that holds under the next probe cycle. The probe names are what you'd grep for in the agents feed.
Players joining mid-stream sit on a spinner for 8–12 seconds before the first segment lands; tail-end viewers reach the live edge eventually, but the cohort joining at peak pays the cost.
Master playlist TTL is tuned for steady-state refresh, not join load; CDN edge nodes have to revalidate a cold variant window on every fresh viewer. playlist.reachable fires green, but the variant.consistency probe races against CDN edge cache fill.
Pin the master playlist to Cache-Control: no-cache, must-revalidate and prime the variant window with a dedicated low-TTL entrypoint. With Streamwake, the agent reads playlist.reachable + variant.consistency on every cadence and pre-warms the variant tree when it sees a join-rate spike.
Viewers report short rebuffer loops every few minutes, though segment.tail_window is passing and the manifest is current. ABR keeps the chosen rung; nothing changes in the timeline.
Origin egress is saturating during traffic peaks — TTFB drifts from a ~300ms baseline into the 1–2s range. The fetch layer is healthy enough that segment bytes arrive, just late enough to empty the player’s buffer. fetch.ttfb_drift flips to warn before any other probe does.
Throttle the probe fan-out during TTFB excursions (fetch.concurrency auto-throttles when it sees the pool near its ceiling), and route the cohort to a healthier egress. Long term: tier the upstream so the live edge isn’t sharing a fleet with VOD.
Player closes and reopens the SourceBuffer at every SCTE-35 cue-out; the timeline jumps and ABR locks the lowest rung for the rest of the break. Viewers see a hard pause even though the segment returns 200.
The packager emits #EXT-X-DISCONTINUITY at every cue even though PTS is continuous across the splice. MSE treats each tag as a forced reset and burns buffer budget. media.discontinuity_resolution flags a discontinuity at every segment boundary, not just at the actual ad boundary.
Emit #EXT-X-DISCONTINUITY only at genuine PTS, codec, or format transitions, and pair it with the SCTE-35 cue so the agent can correlate them. Have the agent tail-window probe assert a discontinuity_tag:scte35_pair ratio within tolerance.
Tail-window probes pass but viewers report short rebuffers near the live edge. Player log shows the next segment always landing 200–400ms later than the schedule predicted.
Declared #EXTINF:6.0 but the packager is producing 6.21s segments because of GOP boundary alignment — the player schedules the next fetch 200ms too early and stalls before the segment is on the wire. segment.tail_window reports the bytes are reachable; it can’t tell the clock is wrong.
Either coerce the packager to honor the declared target duration, or surface actual segment durations in EXTINF so the player uses the real figure. The agent tail-window probe can flag an EXTINF:declared vs EXTINF:actual drift across the trailing window.
Player keeps the dead MEDIA-SEQUENCE even though new segments have landed on origin; ABR locks at the bottom rung. Logs show the master returning 304-not-modified on every refresh.
CDN honored If-Modified-Since against the prior master timestamp even though the live window advanced — a stale 304 hits the player, the variant playlist never advances, and the cohort stays pinned to the lowest rung until a force-refresh.
Set Cache-Control: no-cache, must-revalidate on the master; pin the variant playlists to max-age=1. The playlist.reachable probe flags a 304 against a moving window as a stale-manifest warning before the player’s ABR notices.
Players pinned to a running session suddenly hit a "MEDIA_SEQUENCE_RESET" error or a hard pause; new joiners are fine. The chunklist appears to step backwards after a brief gap.
The packager restarted and rewrote MEDIA-SEQUENCE without an #EXT-X-DISCONTINUITY tag, so running players fetch a segment URI the packager no longer has. segment.hash_drift catches it because the same logical segment has a different SHA across the rotation.
Either emit #EXT-X-DISCONTINUITY any time MEDIA-SEQUENCE resets (this is what the spec says), or keep the sequence monotonic across the restart by draining the prior packager first. The agent tail-window probe asserts MEDIA-SEQUENCE monotonicity across the last 6 segments.
A low-latency HLS workflow lands the partial segments on time, but the player never commits them — latency sits at 8–12s instead of the expected 2–3s. Reports of a "stuck cursor" at peak.
PART-HOLD-BACK drifted away from the delta cursor after a partial-segment rewrite, or CAN-PRELOAD-BEFORE went out of sync with PART-INF timing. The publish path pushed partials at a different cadence than the playlist claims. llhls.part_holdback_drift catches the mismatch.
Pin partial-segment publication to the same clock that drives the chunklist; assert PART-HOLD-BACK matches the delta cursor on every probe cycle. The agent replays the last 3 partials when the cursor looks stuck, which is usually enough to clear the player.
A fraction of viewers — concentrated in one region — see HTTP 403 on the variant playlist; the others are fine. The cohort rolls over to multi-CDN to clear the issue but the underlying signing mismatch keeps recurring.
The signed-URL region key on the alternate CDN is rotated against a different audience than the player’s GeoIP resolves to; the alternate CDN’s WAF rejects the request before the signed query is even validated. fetch.concurrency and playlist.reachable both report a 403 spike against the new edge.
Audit the multi-CDN signing rules so each region’s signing key matches the GeoIP the player claims; have the agent threshold 4xx rates per BaseURL host so consistent 403s trigger a rollback rather than a flapping roll.
Player picks a variant, MSE throws SourceBuffer.appendBuffer with a QuotaExceededError on the first rendition switch; the player falls back to the original ladder rung.
One variant packages CMAF with ftyp brands isom + dash, another with brands isom + cmfv with no overlap. The shared brand set is empty at the joint, so MSE rejects the next segment on switch. The cmaf.ftyp_match probe catches it as soon as the second variant lands.
Align ftyp brand set across every variant (isom + dash + cmfc is the safe union); update the variant TYPE parameter consistently. Probe flips to pass once the brands overlap.
A subset of fetches returns the same URI but a different SHA-256 byte stream than the same URI served from the primary CDN. Viewers on the rolled-out CDN see slightly stale frames on rendition handoff.
A CDN rotation brought up a stale cache layer that’s still serving the pre-rotation byte stream. The headers match (200, content-length, content-type), so a header-level probe reports green; only a byte-level fingerprint catches it. segment.hash_drift fires.
Pin the new CDN to a fresh cache tier (cold pull) and replay from origin. Have the agent re-hash a leading segment from each CDN per probe cycle and alert on fingerprint mismatch before viewer cohorts notice.
A cohort on the affected tier sits calm on a marquee live broadcast — cohort.reconnect_storm_ratio reads at the pre-event baseline — but the origin-shield tier's queue_depth_concurrent pegs to the slot ceiling while edge.egress_kbps stays flat at the expected rate. The cohort is calm; the upstream origin-shield is the driver.
A correlated upstream cache-miss storm — coincident cache.eviction_posture across the warm edge POPs — fans the replay-origin origin_fetch_qps past the tier's pre-provisioned ceiling. cdn_shield.origin_segment_cache_miss_fanin lifts from its pre-event baseline; segment-leg cache-hit on the affected tier shows cache-miss posture on the warm edge POPs but GREEN at the warm cohort's ladder. The cohort is CALM (cohort.reconnect_storm_ratio at baseline, edge.egress_kbps flat).
Shed the correlated cache-misses to a warm edge POP via origin failover; surface the fail-open posture on the shield circuit-breaker to operator-team for approval. See the working postmortem at /incident-lab/origin-shield-saturation-correlated-cache-miss-storm — Tier 0 (who can shed traffic), Tier 1 (who can fail open), Tier 2 (who can escalate), recovery verified cohort-side + shield-side across four staged gates T+30 s → T+15 m, NOT infrastructure-green.
Heuristics: how the agent loop classifies the incident
The probe families above produce verdicts. Three rules in the agentic-ops layer turn a stream of verdicts into an incident classification — without a human reading the timeline.
- Rule: TTFB p95 across the last N probes vs the phased baseline for the same cohort + region-of-origin.
- What it surfaces: origin egress saturation, CDN cold-cache fill on a join spike, regional peering degradation — long before any segment actually 4xxs.
- Agent does next:raise probe cadence for this stream, prepend to the alert feed, and open an incident tagged "egress_pressure" — actionable without waiting for a rebuffer report.
- Rule:in-flight fetches on the outbound pool vs the cohort’s expected fanout; pool-utilization > 85% over more than one cadence.
- What it surfaces: a join storm, or one CDN pulling a disproportionate share of the cohort, or a single player stuck retrying.
- Agent does next: auto-throttle the probe fan-out for this stream, meta-classify as "join_storm vs regional_skew", and emit a routing suggestion rather than a rebuffer alarm.
- Rule: SHA-256 over a leading segment from primary vs backup CDN, vs the hash on the same URI one cadence ago.
- What it surfaces: stale cache tier after a CDN rotation, packaging mismatch surfaced mid-stream, or a redirect chain that re-orders bytes without changing the URI.
- Agent does next:classify "content_drift", switch the probe to hash-validate mode across the leading window, and replay from origin on the affected CDN — before any viewer cohort reports a handoff artifact.
The failure-mode rows above trace back to the three heuristics: intermittent stalls are usually a TTFB drift flagged early; multi-CDN signing-region 403s look like fetch-concurrency exhaustion the moment they spread; packager restarts and CDN rotations surface as segment hash drift before any header-level probe notices. A passive-monitor logs the same verdicts and waits for a rebuffer report to fire — by which point you're already paying the cohort-trust cost and writing the postmortem. The agent loop flips the polarity: classify the verdict under one of the three heuristics, remediate by replaying from origin or re-routing the affected CDN, then verify that the next probe cycle clears. Each heuristic closes a loop that a passive monitoring pipeline cannot.
Diagnose with Streamwake
Register the HLS source 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 an HLS source URL and asks for a 30-second probe cadence. 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 stream is registered, the agents endpoint returns the per-probe verdicts below. The order of checks mirrors the probe families in the section above — playlist.reachable first, variant consistency and segment windows next, then the heuristic layer (fetch.ttfb_drift, fetch.concurrency, segment.hash_drift) and the continuity reconciliation probes last.
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": "HLS",
"probeIntervalSeconds": 30
}'curl https://streamwake.polsia.io/api/v1/agents?stream_id=<id> \
-b "better-auth.session_token=<your-session-cookie>"{
"stream_id": "cklivehlsevent789",
"source": "https://cdn.example.com/live/event/manifest.m3u8",
"protocol": "HLS",
"checks": [
{
"probe": "playlist.reachable",
"result": "pass",
"latency_ms": 132,
"detail": "content-type application/vnd.apple.mpegurl"
},
{
"probe": "playlist.well_formed",
"result": "pass",
"detail": "EXT-X-VERSION=6, MASTER with 3 variants + 1 audio rendition group"
},
{
"probe": "variant.consistency",
"result": "pass",
"variants_checked": 3
},
{
"probe": "segment.head_window",
"result": "pass",
"latency_ms": 96,
"segments_checked": 6
},
{
"probe": "segment.tail_window",
"result": "fail",
"latency_ms": 4128,
"detail": "EXTINF drift at seg-1474: declared 6.0s, actual 6.21s"
},
{
"probe": "segment.hash_drift",
"result": "fail",
"detail": "seg-1474 sha256 mismatch across primary/backup CDN (fingerprint drift)"
},
{
"probe": "fetch.ttfb_drift",
"result": "warn",
"detail": "TTFB p95 1240ms vs phased baseline 360ms — egress saturation suspected"
},
{
"probe": "fetch.concurrency",
"result": "warn",
"detail": "outbound fan-out at 92% of pool ceiling — join-storm signature"
},
{
"probe": "media.discontinuity_resolution",
"result": "pass",
"discontinuities_checked": 0
},
{
"probe": "llhls.part_holdback_drift",
"result": "pass",
"detail": "PART-HOLD-BACK matches delta cursor; CAN-PRELOAD-BEFORE aligned"
}
]
}Read the low-latency arm
The HLS guide above covers the shared master / variant / tag-continuity probes that apply equally to vanilla HLS and LL-HLS. The pair-arm focuses specifically on the low-latency surface — chunked-transfer ingest, partial-segment prefetch, GOP timing, manufacturing-server cadence, playlist-delta sizing, and blocking-playlist-request storms.
Want Streamwake to catch this on its own?
Sign up, register an HLS source, and the same probes that produced the timeline above run on every cadence — and surface in a Slack channel, a webhook, or the streams dashboard.
- Stream list reads from
GET /api/v1/streams; per-stream timeline fromGET /api/v1/agents. - Probe verdicts stream into the dashboard within one cadence interval — manual curl not required.
- Self-serve signup at /sign-up — no sales call required for the first stream.
Related writeups
The closest siblings cover the cache-tier / origin-shield fan-in failure-mode shapes — particularly the correlated upstream cache-miss storm where the cohort stays CALM while the upstream origin-shield fans into the tier past its pre-provisioned ceiling, classified at 84% with cdn_edge_pop_warmup_under_eviction ruled out by name on warm edge POP segment-leg cache-hit. Recovery verified cohort-side + shield-side across four staged gates T+30 s → T+15 m, NOT infrastructure-green.