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, you must simulate what a real player does — not just hammer the manifest endpoint. This page shows how to write a test that follows the full player fetch loop and explains 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 listing all available renditions (bitrates, resolutions). - GET variant/rendition playlist — the player picks a rendition based on the current bandwidth estimate and fetches its 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 discover 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 matters for 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 real player behavior — a viewer consuming video at real speed. Without it, a single VU hammers segments at connection speed, producing unrealistically high throughput per VU and underestimating the number of VUs needed 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); }}Using a tags: { name: 'segment' } on all segment requests groups them into a single “segment” label in the MaxoPerf results breakdown — this makes 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 your key signal of CDN or origin saturation.
Do / don’t
Section titled “Do / don’t”Do:
- Use
think-time/sleep()between segments at the segment duration — this is the most common streaming test mistake; without it, results are 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 — it tests nothing about segment delivery, which is the bulk of streaming load.
- Use a loop that fetches as many segments as possible per second — this does not simulate any real player.
Where to go next
Section titled “Where to go next”- Concurrent viewers load — scale this pattern to thousands of VUs and read the throughput math.
- ABR adaptive bitrate testing — simulate a viewer population spread across multiple renditions.
- Live event streaming load — model the kickoff spike for a live stream.
- Startup time and rebuffering QoE — use MaxoPerf latency to derive QoE metrics.