Most HLS buffering is not a bandwidth problem. It comes from a ladder whose bitrates do not match real throughput, segments that are too long or too short relative to #EXT-X-TARGETDURATION, a buffer target that is too small for a variable network, or variants that are not aligned on the same GOP boundaries. Start by measuring how much forward buffer the player actually holds, then tune the ladder and the client config instead of simply lowering quality.
How it works
An HLS player downloads segments one at a time into a forward buffer. Adaptive bitrate logic estimates available bandwidth from recent segment downloads and picks the highest variant that fits. If the estimate is optimistic, the player requests a rendition it cannot sustain, the buffer drains to zero, and the video stalls while it refills. It then drops to a lower variant, which often produces a visible quality swing.
The playlist defines the rhythm. #EXT-X-TARGETDURATION states the maximum segment duration, and a player typically waits until it has a few segments buffered before it starts. Six-second segments with a ten-second target mean a stall costs at least one full segment to recover. Two-second segments recover faster but add HTTP request overhead and more playlist refreshes. #EXT-X-ENDLIST marks VOD; without it the player follows a live edge and will buffer if it falls too far behind or too close to the edge.
You can watch the buffer directly:
hls.on(Hls.Events.FRAG_BUFFERED, () => {
const v = video;
const buffered = v.buffered.length
? (v.buffered.end(v.buffered.length - 1) - v.currentTime).toFixed(1)
: '0';
console.log('forward buffer:', buffered, 's | level:', hls.currentLevel);
});
video.addEventListener('waiting', () => console.warn('stall at', video.currentTime));
Step-by-step: tune the ladder and the client
1. Build a ladder with sensible spacing. Jumps of 2x between renditions cause oscillation. A typical VOD ladder for a 1080p source is 2400, 1600, 1100, 750, and 450 kbps with matching resolutions. Every variant must share the same GOP length and the same segment boundaries so switching does not produce a decode gap.
# One rendition: fixed 6 s segments, closed GOP at 2 s, aligned keyframes
ffmpeg -i source.mp4 \
-c:v libx264 -preset veryfast -b:v 1600k -maxrate 1740k -bufsize 3200k \
-g 48 -keyint_min 48 -sc_threshold 0 -pix_fmt yuv420p \
-c:a aac -b:a 128k -ac 2 \
-hls_time 6 -hls_playlist_type vod \
-hls_segment_filename "v1600/seg_%05d.ts" v1600/index.m3u8
Repeat for each rung with different -b:v and resolution, then write a master playlist with #EXT-X-STREAM-INF:BANDWIDTH=...,RESOLUTION=... pointing at each variant. Declare the correct BANDWIDTH (including audio): an understated value makes the ABR logic over-select that rendition.
2. Give the client enough buffer.
const hls = new Hls({
maxBufferLength: 30, // seconds of forward buffer to target
maxMaxBufferLength: 60, // hard ceiling under stable conditions
backBufferLength: 30, // keep some played video for seeking back
startLevel: -1, // let ABR choose, or 0 for a safe low start
abrEwmaDefaultEstimate: 800000,
capLevelToPlayerSize: true, // do not fetch 1080p for a 480p element
lowLatencyMode: false // enable only for LL-HLS playlists
});
A low abrEwmaDefaultEstimate makes the player start conservatively and ramp up, which prevents the very first stall on a cold connection. capLevelToPlayerSize avoids wasting bandwidth on pixels the viewer cannot see.
3. Check the delivery path. A CDN that revalidates every segment, or an origin that does not support Range, adds latency to each request. Playlists should be cached briefly (a few seconds for VOD, near zero for live), while segments can be cached for a long time because their URLs are immutable. Verify with curl -w "%{time_starttransfer}" that the first segment arrives promptly.
Common mistakes and parameters
| Symptom | Most likely cause | Check first |
|---|---|---|
| Stall right at start | ABR estimate too optimistic | abrEwmaDefaultEstimate, startLevel |
| Stalls every few minutes | Ladder rung above real throughput | BANDWIDTH values, level switches |
| Quality oscillates | Rungs too far apart | Add intermediate bitrates |
| Switch causes a freeze | Variants not GOP aligned | -g, -sc_threshold 0 |
| Live stream drifts behind | Playing too close to the edge | liveSyncDurationCount |
| Buffering only on one device | Player size vs level caps | capLevelToPlayerSize |
| Setting | Default tendency | Practical guidance |
|---|---|---|
-hls_time | 2 s in many examples | 4-6 s for VOD, 2 s for low latency |
#EXT-X-TARGETDURATION | Must be >= longest segment | Round up, keep constant across variants |
maxBufferLength | 30 s | 20-30 s VOD, lower for live |
backBufferLength | Infinite in some builds | 30-90 s to bound memory |
| Playlist refresh | VOD none, live periodic | Half the segment duration is a common target |
Troubleshooting
- The buffer graph repeatedly hits zero at the same bitrate: that rung is unsustainable; lower its bitrate or remove it from the ladder.
FRAG_LOAD_ERRORappears before each stall: this is a delivery or CORS failure, not ABR; check segment status codes and headers first.- Stalls only after a quality switch: variants have different keyframe intervals; re-encode with a fixed
-gand disabled scene-cut keyframes. - The player starts at the top level and drops immediately: set
startLevel: 0or lowerabrEwmaDefaultEstimatefor a gentle ramp. - Audio continues while video freezes: the container or codec is the problem, not throughput; inspect with
ffprobeand try a different rendition. - Everything is smooth locally but buffers for remote viewers: measure from another network, then look at CDN cache hit ratio and origin response time.
Related reading
- Reproduce the problem in the online HLS player before changing your encoder settings.
- Inspect segment durations and the variant ladder with the stream analyzer.
- Browse all HLS troubleshooting guides for CORS and playlist syntax fixes.
- How to Fix hls.js CORS Errors - diagnose cross-origin and playlist request failures.
- How to Play m3u8 in Chrome (3 Working Methods) - set up hls.js playback in Chrome.
- How to Test an HLS Stream Online - run a full playlist, segment and header check.