Troubleshooting · HLS
Streamwake reliability agents

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.

Protocol: HLS
Format: .m3u8 + TS/fMP4
Streamwake probes: playlist fetch · variant window · segment hash drift · fetch-concurrency · discontinuity reconciliation.
Probes

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.

Playlist fetch
playlist.reachable + playlist.well_formed
fetch · parse
  • Master playlist returns HTTP 200 within the probe budget (default 5s).
  • Content-Type reads as application/vnd.apple.mpegurl — not octet-stream.
  • #EXTM3U on line 1; EXT-X-VERSION matches features used; no BOM.
Variant window
variant.consistency + segment.head_window + segment.tail_window
head · tail
  • Every STREAM-INF variant 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.
Segment hash drift
segment.hash_drift
fingerprint
  • 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.
Fetch-concurrency
fetch.ttfb_drift + fetch.concurrency
concurrency
  • 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.
Discontinuity reconciliation
media.discontinuity_resolution + llhls.part_holdback_drift
continuity
  • #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.
Anatomy

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.

Probes → playlist fields
Quick map from probe verdict to the line you should pull.
  • playlist.reachable → root #EXTM3U
  • variant.consistency #EXT-X-STREAM-INF sibling rows
  • segment.head_window → first #EXTINF segment URI
  • segment.tail_window #EXT-X-MEDIA-SEQUENCE resolved against #EXT-X-PROGRAM-DATE-TIME
  • segment.hash_drift → SHA-256 of the leading .m4s across CDNs
  • llhls.part_holdback_drift #EXT-X-PART-HOLD-BACK vs delta cursor
Master playlist
#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
720p variant playlist
#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.m4s
Failure modes

Ten 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.

01Long join time on first paint
failure mode
Symptom

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.

Root cause

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.

Fix

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.

02Intermittent stalls on otherwise-green segments
failure mode
Symptom

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.

Root cause

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.

Fix

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.

03EXT-X-DISCONTINUITY drift at ad markers
failure mode
Symptom

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.

Root cause

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.

Fix

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.

04EXTINF / partial-segment timing variance
failure mode
Symptom

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.

Root cause

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.

Fix

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.

05Variant-manifest staleness (304 while the live window advanced)
failure mode
Symptom

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.

Root cause

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.

Fix

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.

06EXT-X-MEDIA-SEQUENCE discontinuity after a packager restart
failure mode
Symptom

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.

Root cause

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.

Fix

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.

07Low-latency CMAF edge cases (PART-HOLD-BACK / delta cursor stuck)
failure mode
Symptom

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.

Root cause

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.

Fix

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.

08Multi-CDN signing-region 403 on geographic roll
failure mode
Symptom

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.

Root cause

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.

Fix

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.

09CMAF ftyp brand mismatch on rendition switch
failure mode
Symptom

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.

Root cause

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.

Fix

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.

10Segment hash drift across CDN rotation
failure mode
Symptom

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.

Root cause

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.

Fix

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.

11Stale cache-tier posture across the origin-shield fan-in
failure mode
Symptom

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.

Root cause

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).

Fix

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.

Agentic-ops layer

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.

TTFB drift
fetch.ttfb_drift → egress saturation signal
heuristic
  • 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.
Fetch-concurrency exhaustion
fetch.concurrency → join-storm + pool-ceiling signal
heuristic
  • 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.
Segment hash drift
segment.hash_drift → content / CDN rotation signal
heuristic
  • 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.
Why this reduces MTTR
Active classify → remediate → verify vs humans-on-pager

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

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.

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": "HLS",
    "probeIntervalSeconds": 30
  }'
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": "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"
    }
  ]
}
Pair

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.

Next step

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.

Open the streams dashboard
Auth-gated · reads the timeline the agent wrote on every probe.
  • Stream list reads from GET /api/v1/streams; per-stream timeline from GET /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.
Read the next

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.