Skip to content

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.

A real ABR player follows this sequence for every viewer session:

  1. GET master manifest. A single .m3u8 (HLS) or .mpd (DASH) file lists all available renditions (bitrates, resolutions).
  2. GET variant/rendition playlist. The player picks a rendition from its current bandwidth estimate and fetches that rendition’s segment list.
  3. GET segment loop. The player fetches one segment at a time, at real-time cadence: one GET every segment-duration seconds (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: 4s

The 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.

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 seconds
const 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.

  1. Save your Taurus YAML or k6 .js file locally.
  2. In the MaxoPerf console, navigate to Tests and click New test.
  3. Give the test a name (e.g., “HLS viewer simulation: 500 VU”).
  4. On the Files tab, upload your script. MaxoPerf auto-detects Taurus YAML or k6 based on the file content.
  5. Under Configuration, set the load profile: VUs, ramp-up, hold duration, and regions. Choose regions that match your viewer geography.
  6. Optionally set failure criteria: e.g., p95 segment latency < segment_duration (4000ms). If segment download time exceeds segment duration, real viewers would buffer.
  7. Click Run.

After the run, the Overview tab shows throughput and latency broken down by request label. Look for:

LabelWhat to watch
master-manifestp95 should be under 200ms. Slow manifests delay every session start
variant-playlist-*Should be fast (often CDN-cached). Rising latency signals manifest-server pressure
segmentp95 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:

  • 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.