HLS and DASH manifest and segment testing
HLS (HTTP Live Streaming) and DASH (Dynamic Adaptive Streaming over HTTP) deliver video as a sequence of small HTTP GET requests. To load-test streaming infrastructure accurately, simulate what a real player does instead of hammering the manifest endpoint. This page shows how to write a test that follows the full player fetch loop, and why each step matters.
Before you start
Section titled “Before you start”- Read the Video streaming testing overview for the ABR request model and VOD vs live concepts.
- Read Taurus fundamentals or k6 scripts on Maxoperf for the engine you plan to use.
How a player fetches ABR content
Section titled “How a player fetches ABR content”A real ABR player follows this sequence for every viewer session:
- GET master manifest. A single
.m3u8(HLS) or.mpd(DASH) file lists all available renditions (bitrates, resolutions). - GET variant/rendition playlist. The player picks a rendition from its current bandwidth estimate and fetches that rendition’s segment list.
- GET segment loop. The player fetches one segment at a time, at real-time cadence: one GET every
segment-durationseconds (typically 2–6 s). For live, it also re-fetches the variant playlist regularly to find new segments.
Your load test must reproduce this loop. If you skip the segment loop and only fetch the manifest, you test only the manifest server. You miss the CDN segment load, the origin packaging load and the real latency profile that drives QoE.
Simulating a player session: Taurus example
Section titled “Simulating a player session: Taurus example”execution: - concurrency: 500 # 500 concurrent simulated viewers ramp-up: 2m hold-for: 10m scenario: hls-viewer
scenarios: hls-viewer: requests: # Step 1 — fetch the master manifest - label: master-manifest url: https://cdn.example.com/stream/master.m3u8 method: GET assert: - contains: subject: body value: '#EXTM3U'
# Step 2 — fetch the variant playlist for the 1080p rendition - label: variant-playlist-1080p url: https://cdn.example.com/stream/1080p/playlist.m3u8 method: GET assert: - contains: subject: body value: '#EXTINF'
# Step 3 — segment fetch loop (6 segments = ~24s of video at 4s each) - label: segment-0001 url: https://cdn.example.com/stream/1080p/seg-0001.ts method: GET - label: segment-0002 url: https://cdn.example.com/stream/1080p/seg-0002.ts method: GET think-time: 4s # real-time cadence: wait 4s between segments - label: segment-0003 url: https://cdn.example.com/stream/1080p/seg-0003.ts method: GET think-time: 4s - label: segment-0004 url: https://cdn.example.com/stream/1080p/seg-0004.ts method: GET think-time: 4s - label: segment-0005 url: https://cdn.example.com/stream/1080p/seg-0005.ts method: GET think-time: 4s - label: segment-0006 url: https://cdn.example.com/stream/1080p/seg-0006.ts method: GET think-time: 4sThe think-time: 4s between segment GETs is critical. It models a real player: a viewer watching video at real speed. Without it, a single VU pulls segments as fast as the connection allows. Throughput per VU comes out unrealistically high, and you underestimate how many VUs you need to reach your load target.
Simulating a player session: k6 example
Section titled “Simulating a player session: k6 example”import http from 'k6/http';import { check, sleep } from 'k6';
export const options = { stages: [ { duration: '2m', target: 500 }, // ramp up to 500 concurrent viewers { duration: '10m', target: 500 }, // hold { duration: '1m', target: 0 }, // ramp down ],};
const SEGMENT_DURATION_S = 4; // segment length in secondsconst SEGMENTS_TO_FETCH = 30; // ~2 minutes of simulated playback per VU iteration
export default function () { // Step 1 — master manifest const masterRes = http.get('https://cdn.example.com/stream/master.m3u8', { tags: { name: 'master-manifest' }, }); check(masterRes, { 'master manifest 200': (r) => r.status === 200 });
// Step 2 — variant playlist const variantRes = http.get('https://cdn.example.com/stream/1080p/playlist.m3u8', { tags: { name: 'variant-playlist-1080p' }, }); check(variantRes, { 'variant playlist 200': (r) => r.status === 200 });
// Step 3 — segment loop at real-time cadence for (let i = 1; i <= SEGMENTS_TO_FETCH; i++) { const segUrl = `https://cdn.example.com/stream/1080p/seg-${String(i).padStart(4, '0')}.ts`; const segRes = http.get(segUrl, { tags: { name: 'segment' }, }); check(segRes, { 'segment 200': (r) => r.status === 200, 'segment not empty': (r) => r.body.length > 0, });
// Wait the segment duration before fetching the next one // This is the most important line in the script sleep(SEGMENT_DURATION_S); }}Putting tags: { name: 'segment' } on all segment requests groups them into a single “segment” label in the MaxoPerf results breakdown. That keeps charts readable when you have many segment URLs.
Setting up the test in MaxoPerf
Section titled “Setting up the test in MaxoPerf”- Save your Taurus YAML or k6
.jsfile locally. - In the MaxoPerf console, navigate to Tests and click New test.
- Give the test a name (e.g., “HLS viewer simulation: 500 VU”).
- On the Files tab, upload your script. MaxoPerf auto-detects Taurus YAML or k6 based on the file content.
- Under Configuration, set the load profile: VUs, ramp-up, hold duration, and regions. Choose regions that match your viewer geography.
- Optionally set failure criteria: e.g.,
p95 segment latency < segment_duration (4000ms). If segment download time exceeds segment duration, real viewers would buffer. - Click Run.
How to read the results
Section titled “How to read the results”After the run, the Overview tab shows throughput and latency broken down by request label. Look for:
| Label | What to watch |
|---|---|
master-manifest | p95 should be under 200ms. Slow manifests delay every session start |
variant-playlist-* | Should be fast (often CDN-cached). Rising latency signals manifest-server pressure |
segment | p95 must be below your segment duration. If p95 > segment_duration, real viewers are buffering |
A healthy run shows segment p95 well below the segment duration, steady throughput and near-zero errors. Rising segment latency under load is the main sign of CDN or origin saturation.
Do / don’t
Section titled “Do / don’t”Do:
- Use
think-time/sleep()between segments at the segment duration. Leaving it out is the most common streaming test mistake, and it makes the results meaningless. - Label manifest and segment requests separately so you can distinguish manifest-server from CDN segment issues.
- Use the full player loop: master manifest → variant playlist → segment loop.
Don’t:
- Fetch only the manifest. That tests nothing about segment delivery, which is the bulk of streaming load.
- Use a loop that fetches as many segments as possible per second. No real player behaves that way.
Where to go next
Section titled “Where to go next”- Concurrent viewers load: scale this pattern to thousands of VUs and work through the throughput math.
- ABR adaptive bitrate testing: simulate a viewer population spread across several renditions.
- Live event streaming load: model the kickoff spike for a live stream.
- Startup time and rebuffering QoE: derive QoE metrics from MaxoPerf latency.