Troubleshooting · LL-HLS
Streamwake reliability agents

Tune LL-HLS for low-latency
without losing the live edge.

A working guide to the LL-HLS optimization pitfalls Streamwake catches on real delta playlists — partial-segment timing drift, BLOCKING vs delta request mistakes, CAN-PRELOAD / EXT-X-PRELOAD-HINT placement, SERVER-CONTROL PART-HOLD-BACK tuning, segment vs PART-duration mismatch, and the encoder GOP alignment required to keep PART boundaries on keyframes.

Protocol: HLS
Format: delta + partial segments
Streamwake probes: part cadence · delta · preload · server control · GOP alignment.

Book a technical demo for LL-HLS optimization

Lead magnet
LL-HLS optimization

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 directly reads the playlist and the publish cadence. Every check has a name — that's the name you'll see on the agents feed.

Part cadence
part.timing_consistent
cadence
  • EXT-X-PART-INF:PART-TARGET read off the variant head, divided into observed PART-DURATION jitter.
  • Each PART-DURATION must stay within ±0.05s of PART-TARGET for the whole window.
  • Jitter past the threshold is the smoking gun for encoder / packager cadence drift.
Delta vs blocking
playlist.delta_updates · playlist.blocking
playlist
  • _HLS_msn + _HLS_part match the next part the server returns on every delta request.
  • Delta responses should return under 200B p95; full BLOCKING responses over 3KB are a regression.
  • BLOCKING budget: must complete in < 3 × TARGETDURATION + 1s RTT headroom.
Preload hints
preload.can_preload_hint · preload.hint_for_unpublished_part
preload
  • EXT-X-PRELOAD-HINT placement matches SERVER-CONTROL CAN-PRELOAD-BEFORE.
  • Hints must reference a part the server has NOT yet published or that sits inside the PART-HOLD-BACK window.
  • Mismatched hint vs SERVER-CONTROL is a fail — the hint is misleading the player.
Server timing
server_control.target_latency_window · server_control.can_block_reload
ll-hls server
  • EXT-X-SERVER-CONTROL: CAN-BLOCK-RELOAD + PART-HOLD-BACK + HOLD-BACK read from the variant head.
  • Measured playback-head window must sit within PART-HOLD-BACK (no over-buffering or starvation).
  • Missing CAN-BLOCK-RELOAD regresses the connection to BLOCKING-only; the probe fails on any variant that drops the field.
Encoder GOP alignment
gop.boundary_aligned · encoder.cadence_part_compatible
encoder · gop
  • Every keyframe must sit at the start of an EXT-X-PART — mid-PART IDR is a re-buffer risk.
  • GOP period must divide cleanly into TARGETDURATION; 2.0s GOP with 6s TARGET makes for 6 parts per GOP.
  • Encoder cadence drift > ±0.05s fails the probe — packager cannot honor PART-TARGET.
Anatomy

Anatomy of an LL-HLS media playlist

A realistic LL-HLS variant playlist with PART-TARGET=0.33, SERVER-CONTROL declaring CAN-BLOCK-RELOAD + PART-HOLD-BACK, a full segment of 18 PARTs, and a PRELOAD-HINT for the next part. The probe table on the left names exactly which field each probe reads.

LL-HLS variant (partial segments + delta)
#EXTM3U
#EXT-X-VERSION:9
#EXT-X-TARGETDURATION:6
#EXT-X-PART-INF:PART-TARGET=0.33
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=1.0,HOLD-BACK=4.0
#EXT-X-MEDIA-SEQUENCE:1237
#EXT-X-MAP:URI="init.mp4"
#EXTINF:6.000,
seg1237.m4s
#EXT-X-PROGRAM-DATE-TIME:2026-08-06T10:42:10.000Z
#EXT-X-PART:DURATION=0.330,URI="seg1237.0.m4s"
#EXT-X-PART:DURATION=0.330,URI="seg1237.1.m4s"
#EXT-X-PART:DURATION=0.330,URI="seg1237.2.m4s"
#EXT-X-PART:DURATION=0.330,URI="seg1237.3.m4s"
#EXT-X-PART:DURATION=0.330,URI="seg1237.4.m4s"
#EXT-X-PART:DURATION=0.330,URI="seg1237.5.m4s"
#EXT-X-PART:DURATION=0.330,URI="seg1237.6.m4s"
#EXT-X-PART:DURATION=0.330,URI="seg1237.7.m4s"
#EXT-X-PART:DURATION=0.330,URI="seg1237.8.m4s"
#EXT-X-PART:DURATION=0.330,URI="seg1237.9.m4s"
#EXT-X-PART:DURATION=0.330,URI="seg1237.10.m4s"
#EXT-X-PART:DURATION=0.330,URI="seg1237.11.m4s"
#EXT-X-PART:DURATION=0.330,URI="seg1237.12.m4s"
#EXT-X-PART:DURATION=0.330,URI="seg1237.13.m4s"
#EXT-X-PART:DURATION=0.330,URI="seg1237.14.m4s"
#EXT-X-PART:DURATION=0.330,URI="seg1237.15.m4s"
#EXT-X-PART:DURATION=0.330,URI="seg1237.16.m4s"
#EXT-X-PART:DURATION=0.330,URI="seg1237.17.m4s"
#EXTINF:6.000,
seg1238.m4s
#EXT-X-PROGRAM-DATE-TIME:2026-08-06T10:42:16.000Z
#EXT-X-PART:DURATION=0.330,URI="seg1238.0.m4s"
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="seg1239.0.m4s"
BLOCKING-replay trace (delta should have fired)
GET /live/event/llhls-variant.m3u8?_HLS_msn=1239&_HLS_part=2
HTTP/2 200
content-type: application/vnd.apple.mpegurl
date: Thu, 06 Aug 2026 10:42:17.500Z
cache-control: max-age=0
server-timing: _HLS_msn=1239; _HLS_part=2; part_drift_ms=0
x-cache: MISS

# returning full delta UPDATE the server could have shipped
# in a header-only response — 4.9KB returned, ~80 bytes needed
Encoder cadence vs PART-TARGET (PART-DURATION jitter)
# Encoder cadence vs EXT-X-PART-INF
expected_part_target=0.330
observed: 0.000, 0.330, 0.330, 0.330, 0.660, 0.330,
0.330, 0.330, 0.330, 0.330, 0.330, 0.330,
0.330, 0.330, 0.330, 0.660, 0.330, 0.990
# PART-DURATION jitter on the 0.660 and 0.990 entries —
# encoder GOP slipped and dragged the parts past PART-TARGET.
Probes → playlist fields
Quick map from probe verdict to the playlist field you should grep.
  • part.timing_consistent EXT-X-PART DURATION vs EXT-X-PART-INF PART-TARGET
  • playlist.delta_updates _HLS_msn, _HLS_part, response body size
  • playlist.blocking→ request method + payload size + RTT (BLOCKING > 3KB after a delta cursor exists = fail)
  • preload.can_preload_hint EXT-X-PRELOAD-HINT TYPE plus CAN-PRELOAD-BEFORE
  • server_control.target_latency_window EXT-X-SERVER-CONTROL fields
  • gop.boundary_aligned EXT-X-PART start times vs GOP keyframe timestamps

The variant playlist is the canonical LL-HLS shape: EXT-X-PART-INF:PART-TARGET=0.33 tells the player where its parts will land, EXT-X-SERVER-CONTROL opens the BLOCKING channel and gates PART-HOLD-BACK, and every EXT-X-PART entry is one PART-TARGET of media the player can fetch out of order.

The whole idea of LL-HLS is that the client can keep requesting small deltas — the _HLS_msn + _HLS_part query on the GET line. When the server returns a tiny ~80-byte delta, latency stays low. When a delta cursor breaks and the client requests a full BLOCKING reload (the trace above), the server sends back the full playlist and the player stalls for the BLOCKING budget — playlist.blocking flags the regression.

The part-cadence trace on the left is the encoder drift trace: PART-DURATION oscillates past PART-TARGET (one cycle to 0.660, one cycle to 0.990). That's the exact pattern the agent surfaces under encoder.cadence_part_compatible. The fix is almost always encoder-side: lock GOP duration against PART-TARGET × N so the packager cannot be asked to fill a part from a 2.5s GOP when the target is 0.33s.

Why delta > BLOCKING
The whole point of LL-HLS is that deltas ship under 200B.

A delta response is the new + the last MEDIA-SEQUENCE worth of deltas — typically a single new EXT-X-PART line plus the trailing EXT-X-PRELOAD-HINT. A BLOCKING response is the entire served window back to the playback head. The probe counts bytes: > 3KB on a delta cursor means the server failed to honor CAN-BLOCK-RELOAD.

Failure modes

Ten ways LL-HLS optimization goes wrong

Each row: symptom the player 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.

01PART-DURATION drifts past PART-TARGET — part cadence broken
failure mode
Symptom

Player sees PART inflight but the next PART arrives 0.6–0.9s after the previous one instead of the declared PART-TARGET=0.33. Latency rises sharply; the player stutters because it waits for parts that the encoder dragged longer.

Root cause

Encoder GOP slipped out of rhythm with the partial-segment packager. EXT-X-PART-INF declared PART-TARGET=0.33 but the encoder emits 2.5s GOPs under load, and the packager is forced to fill each PART with ~0.66s of media. part.timing_consistent flags the proportional drift.

Fix

Tighten the encoder cadence so GOP duration is an exact multiple of PART-TARGET (PART-TARGET=0.33 with GOP=2.0 aligns as 6 parts per GOP). Reconfigure the packager to refresh PART-DURATION each cycle; the part.timing_consistent probe reports bounded jitter once the cadence is in lock-step.

02BLOCKING request when delta should fire — over-fetch on the live edge
failure mode
Symptom

Edge logs show the player issuing a full BLOCKING playlist GET every refresh instead of a delta request. Cache misses spike; bandwidth spent on each refresh is 4–5KB when ~80B would have been enough.

Root cause

The client lost its delta cursor — usually because _HLS_msn/_HLS_part drifted, the server missed a delta window, or the client believed the playlist was stale and triggered a reload. CAN-BLOCK-RELOAD lets the server hold the connection; the client gave up too early.

Fix

Tune the delta-timeout on the packager to match the BLOCKING budget (default 3s); when a BLOCKING request returns the full playlist, the server-control_can_block_reload probe should flip to fail and streamwake should report the over-fetch ratio.

03CAN-PRELOAD says false but EXT-X-PRELOAD-HINT is present
failure mode
Symptom

A preload hint is in the playlist — TYPE=PART for the next part the encoder is about to publish — but the SERVER-CONTROL block says CAN-PRELOAD-BEFORE=false (or is missing). Players that trust the hint issue a request, the 404 fires, and the player mis-attributes the failure.

Root cause

Packager inconsistency: the hint tag is being emitted by the partial-segment emitter, but the SERVER-CONTROL block was authored by a different upstream module that does not "see" the same publish pipeline. The two pieces disagree on whether preload is allowed.

Fix

Make SERVER-CONTROL authoritative — every playlist writer must read CAN-PRELOAD-BEFORE from the same source as the hint emitter. preload.can_preload_hint should fail across any playlist where the hint and the SERVER-CONTROL disagree.

04PART-TARGET and segment duration don't fit — discontinuity on the boundary
failure mode
Symptom

Every few segments the player drops into a discontinuity. PDT jumps by 0.5–1.0s at the boundary, even though no EXT-X-DISCONTINUITY tag is in the playlist. ABR ladder descends because the time mismatch breaks the buffer math.

Root cause

TARGETDURATION=6 was set but the packager emitted 6.5s segments. PART-TARGET=0.33 over 6s = 18 parts, but 18 × 0.33 = 5.94 < 6, leaving ~60ms unaccounted for; the packager rolled the leftover into the next segment, shifting PDT.

Fix

Either tune PART-TARGET so the residual is divisible (18 × 0.33 = 5.94 → PART-TARGET=0.333 for 18 parts = 6.00), or lower TARGETDURATION to PART-TARGET * floor(segment / PART-TARGET). The part.timing_consistent probe must flag mismatches between TARGETDURATION and PART-TARGET * N.

05Encoder GOP not aligned to PART boundaries — mid-PART keyframe
failure mode
Symptom

Player reports frequent re-buffer events at the start of every sixth part. Codec profiler shows a keyframe mid-PART, so the player rolls forward to the next IDR but the partial append is missing the leading frame.

Root cause

Encoder GOP period and packager PART cadence are not locked. The encoder emits a 2.0s GOP, but the packager is splitting on inference — parts occasionally start in the middle of a frame. gop.boundary_aligned fails whenever a keyframe index lands inside a PART instead of at its boundary.

Fix

Lock the encoder to a fixed GOP that divides evenly into TARGETDURATION, hand that cadence to the packager, and force partial splitters to start at every keyframe. The gop.boundary_aligned probe flips to pass when every PART starts with an IDR.

06Missing #EXT-X-SERVER-CONTROL on a LL-HLS variant
failure mode
Symptom

The variant playlist lacks the SERVER-CONTROL block entirely (no PART-HOLD-BACK, no CAN-BLOCK-RELOAD). Players default to legacy HLS timing, stop asking for delta updates, and revert to a full BLOCKING request every refresh.

Root cause

The packager detected the partial-segment emission but did not emit the SERVER-CONTROL block at the variant head. Most often a config-flag (SERVER-CONTROL_ENABLED=false), or the playlist is being proxied by an upstream cache that dropped the line.

Fix

Make the SERVER-CONTROL block a required emission on every LL-HLS variant. server_control.target_latency_window should fail on any variant missing CAN-BLOCK-RELOAD or PART-HOLD-BACK — both fields are required for the delta vs reload decision.

07PART-HOLD-BACK set wrong — over-buffered or starved live edge
failure mode
Symptom

Latency glass shows either 8–10 seconds of glass-to-glass delay (over-buffered) or 0.3 seconds (starved, dropping in the playback core). Both happen on the same source within minutes of each other.

Root cause

PART-HOLD-BACK was tuned once and never re-tuned. As segment duration shifted (4s → 6s) and GOP cadence was changed to accommodate higher resolutions, the original PART-HOLD-BACK value either over-anchored the playback head or let it chase the live edge too aggressively.

Fix

Tune PART-HOLD-BACK against the measured playback-head window — the agent reports the actual window in server_control.target_latency_window. As a rule, PART-HOLD-BACK should sit between 1.0× and 1.5× the measured jitter; tune up if over-buffered, down if starved.

08_HLS_part staleness — client cursor stuck on an old part number
failure mode
Symptom

Player keeps asking for a part the server already deleted from the playlist. Server returns 410 Gone or a full reload. Cursor TTL is permanently wrong because the player believes it is still behind.

Root cause

Client cursor (_HLS_msn/_HLS_part) advanced past the server's bookkeeping window — usually because the client was paused and the server removed 30+ parts from the tail of the playlist while paused. The cursor is no longer valid; the server cannot fulfill a delta, so it returns 410.

Fix

Force a full playlist reload after any pause > PART-HOLD-BACK × 3, or have the agent monitor part-cursor deltas and prompt a reload proactively. The playlist.delta_updates probe should treat 410 responses as fail and run a fresh playlist fetch.

09BLOCKING playlist request times out — client stall on the reload
failure mode
Symptom

PLAYER_LOGS show playlist_load timeout at 6s; the player stalls for the reload window, then attempts a full BLOCKING request, which itself times out. Latency spikes to ~12s before the first frame re-paints.

Root cause

CAN-BLOCK-RELOAD=YES but the BLOCKING budget is too short for the edge round-trip — typically because edge POP RTT rose under load, or the playlist itself grew past the budget due to long EXTINF lists. blocking-playlist.timeout fires before the playlist returns.

Fix

Raise the BLOCKING budget (the HLS spec default is 3 * TARGETDURATION), or move the player to a delta-first workload under higher POP RTT. blocking-playlist.timeout should flag timeouts shorter than 3× TARGETDURATION + 1s RTT headroom.

10EXT-X-PRELOAD-HINT references a part already published
failure mode
Symptom

Player preloads a part the encoder already finished; cache miss for the part, then a redundant fetch right after. HTTP request rate roughly doubles on the live edge during preload windows.

Root cause

Packager emitted the preload hint just before becoming aware that the part had already been published (only possible with a 0.0s PART-TARGET or an encoder publishing ahead of PART-HOLD-BACK). The hint references a URL the player no longer needs.

Fix

Filter hints against the published MSD — preload.can_preload_hint rejects any hint whose URI is already covered by an EXTINF in the playlist. The probe should report hint_vs_published_uri diff every cycle; resolve by aligning the packager pipeline.

Diagnose

Diagnose with Streamwake

Register the LL-HLS 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 — part cadence, delta vs BLOCKING, preload, server control, GOP alignment.

The curl below registers an LL-HLS source URL with a 30-second probe cadence. The HLS+LL protocol routing makes the agent run the part cadence / delta / preload / server control / GOP probes on every refresh, plus the agent timeline the curl below lists back is the canonical verdicts summary you'll see when the playlist is in a healthy delta mode with one warning lane.

The cookie is the same better-auth.session_token that gates every /api/v1/* call — see the auth guide for how to mint one.

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/llhls-variant.m3u8",
    "protocol": "HLS+LL",
    "probeIntervalSeconds": 30,
    "agents": [
      "part.timing_consistent",
      "playlist.delta_updates",
      "playlist.blocking",
      "preload.can_preload_hint",
      "preload.hint_for_unpublished_part",
      "server_control.target_latency_window",
      "server_control.can_block_reload",
      "gop.boundary_aligned",
      "encoder.cadence_part_compatible"
    ]
  }'
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": "cklivellhlsplanner891",
  "source": "https://cdn.example.com/live/event/llhls-variant.m3u8",
  "protocol": "HLS+LL",
  "playlist_mode": "delta",
  "checks": [
    {
      "probe": "part.timing_consistent",
      "result": "pass",
      "detail": "PART-DURATION stays within +/-0.05s of PART-TARGET=0.330 across 18 parts in seg1237; part drift bounded"
    },
    {
      "probe": "playlist.delta_updates",
      "result": "pass",
      "delta_responses": 24,
      "delta_bytes_p50": 78,
      "delta_bytes_p95": 142,
      "detail": "next_msn matches _HLS_msn in 24/24 probes; delta headers return under 200B p95"
    },
    {
      "probe": "playlist.blocking",
      "result": "warn",
      "blocking_requests": 2,
      "blocking_ms_p95": 3840,
      "detail": "client fell back to a BLOCKING (full playlist) request twice — DNS hiccup, retry took 3.84s (above 3s BLOCKING budget)"
    },
    {
      "probe": "preload.can_preload_hint",
      "result": "pass",
      "detail": "CAN-PRELOAD-BEFORE=YES in client manifest; EXT-X-PRELOAD-HINT TYPE=PART present for seg1239.0.m4s"
    },
    {
      "probe": "preload.hint_for_unpublished_part",
      "result": "pass",
      "detail": "preload hint references a part the server has not yet published — align with PART-HOLD-BACK=1.0s window"
    },
    {
      "probe": "server_control.target_latency_window",
      "result": "pass",
      "part_hold_back_s": 1.0,
      "hold_back_s": 4.0,
      "measured_window_s": 1.04,
      "detail": "PART-HOLD-BACK=1.0 matches the measured playback-head window — no over-buffering"
    },
    {
      "probe": "server_control.can_block_reload",
      "result": "pass",
      "can_block_reload": "YES",
      "detail": "CAN-BLOCK-RELOAD=YES — client may hold the connection open for delta updates"
    },
    {
      "probe": "gop.boundary_aligned",
      "result": "pass",
      "gop_period_s": 2.0,
      "part_count_per_gop": 6,
      "detail": "every keyframe sits at the start of an EXT-X-PART — no mid-PART GOP slip on the captured window"
    },
    {
      "probe": "encoder.cadence_part_compatible",
      "result": "fail",
      "detail": "encoder GOP slipped to ~2.5s for two cycles; PART-DURATION jitter to 0.660 and 0.990 — PART-INF PART-TARGET=0.330 cannot be honored"
    }
  ]
}
Next step

Want Streamwake to catch this on its own?

Sign up, register an LL-HLS probe, and the same cadence + delta + preload + server control + GOP 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: LL-HLS optimization.)
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.
Back to library

Read the next protocol guide

The other live entries cover the canonical protocol-by-protocol failure patterns.