HLS Bandwidth Estimation Explained: EWMA, Throughput Measurement, and How ABR Picks a Quality
August 18, 2026 · 7 min read
Key takeaways
- HLS players estimate available bandwidth using an exponentially weighted moving average (EWMA) of recent segment download throughputs — not a simple average.
- hls.js maintains two EWMAs with different decay factors: a fast one (α ≈ 0.72) that reacts quickly to drops, and a slow one (α ≈ 0.97) that tracks sustained capacity.
- Buffer level is a second input to ABR decisions: even if bandwidth looks high, a near-empty buffer causes a defensive quality drop; a full buffer permits an upgrade.
- Quality switches are asymmetric by design — drops happen immediately on a threshold breach, while upgrades require the estimate to stay high for several consecutive segments.
- The /convert page bypasses ABR entirely: it picks the highest-BANDWIDTH variant from the master playlist once and downloads every segment at that fixed quality.
When an HLS player decides whether to switch from 720p to 1080p — or drop back down mid-stream — it is not reading a number from the server. It is making a statistical inference from the download history of the last few segments, filtered through an algorithm designed to be wrong slowly rather than right occasionally. That algorithm is the exponentially weighted moving average, and understanding it explains both why adaptive streaming feels smooth most of the time and why it sometimes makes infuriating choices.
Raw throughput is too noisy to act on directly
Each HLS segment is a few seconds of video, typically 2–6 MB. The time it takes to download tells you the throughput for that segment: bytes / duration_seconds. In theory, that number tells you the available bandwidth. In practice, it tells you the bandwidth at that exact moment, subject to TCP slow-start, CDN edge-server load, WiFi retransmits, and whatever else your network was doing at the time.
If a player switched quality every time a single segment was slow, a brief interference burst on a WiFi channel would cause a visible quality drop for no real reason. If it required the estimate to stay low for many segments before switching down, a genuine congestion event would drain the buffer before the player responded. The EWMA is the tradeoff between these two failure modes.
The EWMA formula
Each new throughput sample updates the estimate according to:
new_estimate = α × sample + (1 − α) × old_estimatewhere α (the smoothing factor) controls how quickly the estimate responds to new data. An α close to 1 gives almost all the weight to the latest sample — the estimate reacts instantly but is noisy. An α close to 0 barely moves — the estimate is stable but lags far behind reality. The “half-life” of an old sample is the number of new samples it takes for its contribution to fall below 50%: approximately log(0.5) / log(1 − α) samples.
Two EWMAs: fast and slow
hls.js does not use a single EWMA. It maintains two in parallel, each with a different half-life. The exact values vary by version, but the logic is consistent:
- Fast EWMA (half-life ≈ 3 seconds of wall-clock download time): reacts quickly to drops. If the last two segments were slow, the fast estimate has already fallen.
- Slow EWMA (half-life ≈ 9 seconds): tracks sustained throughput. It takes a run of slow segments to move it significantly.
When selecting a quality variant, the ABR controller uses the minimum of the two. This asymmetry is intentional: a single fast segment cannot trick the slow estimate into an upgrade, but a single slow segment immediately pulls the fast estimate down and forces re-evaluation. The player is pessimistic about good news and responsive to bad news.
Buffer level as a second dimension
Bandwidth estimates alone can still produce a bad experience. Imagine the estimate says 8 Mbps is safely achievable, but the buffer has drained to 1.5 seconds while you were switching between WiFi networks. Playing a 1080p segment that takes 3 seconds to download — even at 8 Mbps — would cause a stall before it finishes.
hls.js addresses this with buffer-based overrides. When the buffer level drops below a configurable low-water threshold (often 4 seconds in default configs), the ABR logic ignores the bandwidth estimate entirely and selects the lowest available quality — the goal becomes refilling the buffer as fast as possible, not playing at the best quality. When the buffer is comfortably full (above the high-water mark, often 12–15 seconds), the algorithm can attempt an upgrade sooner than the EWMA alone would suggest, because there is time to recover if the upgrade fails.
Asymmetric switching: down fast, up slowly
One of the most deliberate design choices in HLS ABR is the asymmetry between downward and upward quality switches. A quality drop happens immediately when the estimate breaches a threshold — usually when the current quality's BANDWIDTH attribute is above 0.85–0.9× the estimated throughput, leaving a safety margin. An upgrade, by contrast, requires the estimate to have been above the next quality's BANDWIDTH threshold for several consecutive segments.
This asymmetry exists because the cost of the two errors is not symmetric. A failed upgrade (selecting 1080p when only 720p fits) drains the buffer and may cause a stall — visible and annoying. A delayed upgrade (staying at 720p when 1080p would have worked) just means a slightly less sharp picture — invisible unless the viewer is paying attention. Rational players optimize for not stalling.
What a master playlist actually provides
The BANDWIDTH attribute on each #EXT-X-STREAM-INFline is the maximum bit rate of that variant, measured in bits per second. It is not a guarantee — a segment's actual download rate depends on the CDN, the viewer's ISP, and the encoding complexity of that specific scene. The ABR algorithm compares its throughput estimate against these declared values with a safety margin to decide which variant is safe to select. See How Adaptive Bitrate Streaming Works in HLS for how the BANDWIDTH and AVERAGE-BANDWIDTH attributes relate to each other and how hls.js uses both.
How /convert skips all of this
The /convert page does not do adaptive streaming. It parses the master playlist once, sorts variants by BANDWIDTH descending, and picks the first one — the highest declared quality. Then it downloads all segments of that single variant sequentially and muxes them into an MP4. There is no EWMA, no buffer level, no quality switch. If the highest quality is 10 Gbps (hypothetically), the converter attempts it regardless of actual download speed.
This is the right trade-off for a downloader. A player optimises for uninterrupted real-time playback; a converter optimises for the highest possible output quality and is willing to take longer to get it. The worst that happens is the download is slow — not a stall the viewer notices. For the quality selection details, see How the Converter Picks the Highest HLS Quality.
Observing ABR in the wild
The easiest way to watch EWMA-driven ABR in action is the hls.js demo page. Open the browser DevTools Network panel, start a stream, and throttle your connection to a low bandwidth using the DevTools network throttling presets. Watch the segment URLs shift from a high-resolution path to a lower one over the course of 3–5 segments — that delay is the EWMA accumulating enough evidence before committing to a quality drop. Release the throttle and watch the upgrade happen more gradually: several good segments before the quality climbs back up.
You can also see raw throughput numbers by enabling hls.js debug logging in the console. Each segment logs its byte count, download duration, and the resulting throughput figure that feeds into the EWMA — the same number the ABR controller reads when deciding what to play next.
Questions & answers for AI agents
Short, direct answers an assistant can quote or summarize.
What is EWMA and why do HLS players use it for bandwidth estimation?
An exponentially weighted moving average weights recent throughput samples more heavily than older ones, so the estimate tracks current conditions without overreacting to a single slow segment. Simple averages treat a segment downloaded three minutes ago the same as one downloaded a second ago — EWMA lets the estimate decay toward the present.
Why does hls.js use two EWMAs instead of one?
The fast EWMA (short half-life) detects bandwidth drops quickly so the player can switch down before the buffer drains. The slow EWMA (long half-life) avoids unnecessary upgrades triggered by a single fast segment. ABR logic uses the minimum of the two, which means it reacts aggressively to bad news and conservatively to good news.
What role does buffer level play in quality selection?
Buffer level acts as a safety override on top of the bandwidth estimate. If the playback buffer is below a low-water threshold (often 4–8 seconds), the player selects a lower quality even if the bandwidth estimate suggests a higher one would succeed. If the buffer is above a high-water mark, it may upgrade quality sooner than the bandwidth estimate alone would justify.