Low-Latency HLS Explained: Playlist Polling, EXT-X-PART, and What lowLatencyMode Does in hls.js
July 13, 2026 · 7 min read
How HLS streaming works mentions, in one paragraph, that a live media playlist has no #EXT-X-ENDLISTtag and "the player has to keep re-fetching it every few seconds as new segments are appended." That's true, but it glosses over two things worth spelling out: exactly how that re-fetching is supposed to work, and how much end-to-end delay it bakes in by design — delay that a newer set of extensions, grouped under the name Low-Latency HLS (LL-HLS), exist specifically to cut down.
How an ordinary live playlist gets refreshed
A live media playlist carries an #EXT-X-TARGETDURATION tag — the maximum length any segment in it will have — and RFC 8216 uses that number as the basis for how often a client is supposed to reload the playlist. The rule it lays out is simple: after loading the playlist, wait at least the target duration before reloading it again; if a reload comes back unchanged, wait at least half the target duration before trying again. A player isn't polling as fast as it can — it's polling roughly as often as a new segment could plausibly have shown up, backing off further once it's confirmed nothing new has arrived yet.
Each reload also carries an #EXT-X-MEDIA-SEQUENCE tag — the sequence number of the first segment still listed. A live playlist is a sliding window: as the encoder produces new segments and appends them to the end, the server drops the oldest ones off the front and the media sequence number climbs to match. A player diffs the new playlist against the last one it saw, finds the segments it doesn't have yet by sequence number, and fetches only those — not the whole window again.
The latency that design bakes in
Add it up and a segment sits on the server for a meaningful stretch before a player even learns it exists: the segment itself has to finish encoding (up to a full TARGETDURATIONworth of video, since a segment isn't written until it's complete), then the player's next scheduled poll has to land, then the player typically buffers a few segments deep before it starts playing them — a distance hls.js configures with liveSyncDurationCount, the number of segments back from the live edge it targets. Stack a 6-second target duration, a poll interval measured in seconds, and a multi-segment sync buffer, and 20–30+ seconds of glass-to-glass delay between something happening on camera and it appearing on screen is ordinary for plain HLS — fine for most broadcasts, not fine for anything that needs to feel like a phone call or track a live score in near real time.
What Low-Latency HLS changes: segments the player doesn't have to wait for
LL-HLS attacks that delay at its source: instead of making a player wait for a whole segment to finish before it can be listed and fetched, the server can publish it in pieces as it's written.#EXT-X-PART tags describe those pieces — partial segments with their own DURATION and URI, typically a fraction of a second to roughly a second each, that together make up one ordinary segment once it's complete. A part can carry its own BYTERANGE attribute too, so a still-growing file can be addressed one slice at a time rather than requiring a separate object per part. An INDEPENDENT attribute marks which parts a decoder can actually start on without an earlier part in hand, since not every partial segment begins on a clean random-access point.
Ahead of a part actually finishing, the server can also publish an #EXT-X-PRELOAD-HINT tag — announcing the URI of a part (or an #EXT-X-MAPinitialization segment) that's still being written, so a client can open the request early and start receiving bytes the moment they exist rather than waiting for a playlist reload to even mention the part is done.
Blocking playlist reload: turning polling into a long-poll
Partial segments alone don't help if the player still has to guess when to reload the playlist. LL-HLS replaces that guesswork with blocking playlist reload: a client appends _HLS_msn (and, for a specific part, _HLS_part) query parameters to the playlist URL, naming the exact media sequence number and part index it wants next. Instead of returning immediately, the server holds the request open until that part actually exists, then responds. The client isn't polling at all in the traditional sense anymore — it's making one request per part it needs and letting the server decide when to answer, which collapses the poll-then-wait-then-poll-again cycle into a single round trip per part.
A playlist-level #EXT-X-SERVER-CONTROL tag is what makes any of this negotiable rather than assumed: its CAN-BLOCK-RELOAD attribute tells a client whether the server actually supports blocking reload at all, and its PART-HOLD-BACK attribute recommends how many parts back from the live edge a low-latency client should target — the LL-HLS equivalent of liveSyncDurationCount, published by the server instead of configured by the player.
What lowLatencyMode actually turns on in this site's player
VideoPlayer.tsx constructs hls.js with lowLatencyMode: live, where live is true exactly when /play was opened with broadcast=1 — the flag the extension sets when it opens a tab for a broadcast rather than an on-demand video. Flipping that on tells hls.js to look for and honor the LL-HLS tags above — parsing #EXT-X-PART and #EXT-X-PRELOAD-HINT entries and issuing blocking reloads when #EXT-X-SERVER-CONTROL advertises CAN-BLOCK-RELOAD — rather than treating every playlist as plain, part-free HLS. Whether that produces any real latency improvement is entirely up to the source: a stream whose playlist never emits #EXT-X-PART tags in the first place gives hls.js nothing low-latency to do, and it falls back to ordinary target-duration polling regardless of the flag. live also sets liveSyncDurationCount: 3, so the player targets a position three segments behind whatever edge it finds — low-latency parts or ordinary full segments, same setting either way.
The same live flag hides the native <video> controls entirely (controls={!live}). That's a deliberate, separate choice from anything LL-HLS-specific: a browser's built-in scrubber assumes a fixed, known duration to draw a seek bar against, and a live stream's duration is a moving target that grows with every playlist reload — there's no meaningful "end" to seek toward, so the player drops the control instead of showing a scrubber that can't represent the stream honestly.
What this site doesn't do itself
Every mechanism above — target-duration polling, sliding-window sequence diffing, partial-segment parsing, blocking reload — is implemented inside hls.js, not in this site's own code. lib/m3u8-parser.ts's parseMedia() has no awareness of #EXT-X-PART, #EXT-X-PRELOAD-HINT, or #EXT-X-SERVER-CONTROL at all, because /play never runs its own playlist parser on a live stream — it hands the raw URL straight to Hls.loadSource()and lets the library own every reload from there. That's a meaningfully different arrangement than /convert, which does run its own parser but only ever expects a static, #EXT-X-ENDLIST-terminated playlist — pointing the converter at a live broadcast isn't a supported flow, since a one-pass "fetch every segment once" job has no sensible endpoint against a playlist that never stops growing.
FAQ
Does every live HLS stream support Low-Latency HLS? No. LL-HLS tags are something the encoder/packager has to choose to emit; plenty of live playlists in the wild are still plain HLS with no #EXT-X-PART or #EXT-X-SERVER-CONTROL at all, and hls.js falls back to ordinary polling against those exactly as if lowLatencyMode were off.
Is a media playlist with no #EXT-X-ENDLIST always a live stream? In practice yes — the absence of #EXT-X-ENDLIST is precisely how a player is supposed to tell a live playlist from a VOD one. This site relies on the broadcast=1 parameter from the extension instead of inspecting the playlist itself, since the extension already knows which kind of stream it detected before the tab even opens.
Does blocking playlist reload mean the player only ever makes one request? No — it's one held-open request per part or segment the player still needs, not a single persistent connection for the whole session. Each response resolves as soon as the requested _HLS_msn/_HLS_part becomes available, and the player immediately issues the next one for whatever comes after it.