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

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

  1. GET master manifest — a single .m3u8 (HLS) or .mpd (DASH) file listing all available renditions (bitrates, resolutions).
  2. GET variant/rendition playlist — the player picks a rendition based on the current bandwidth estimate and fetches its 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 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: 4s

The 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 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);
}
}

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.

  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 your key signal of CDN or origin saturation.

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.