How does your live origin answer a low-latency HLS player?
A playlist can declare CAN-BLOCK-RELOAD=YES while the CDN in front of it caches the playlist without its query string — and every player falls back to polling, seconds behind live. latencyprobe acts as the player, times the answers, and checks each against the HLS spec.
go install github.com/Allan-Nava/latencyprobe/cmd/latencyprobe@latestOr a static binary for Linux, macOS and Windows from the latest release, or docker run --rm ghcr.io/allan-nava/latencyprobe <url>. No ffmpeg, no cgo, no dependencies.
$ latencyprobe https://cdn.example/live/master.m3u8 🔴 BAD server-control PART-HOLD-BACK=0.8s is below twice PART-TARGET (1s) [§4.4.3.8] 🔴 BAD delta _HLS_skip=YES returned a full playlist (16s listed, Skip Boundary 12s) [§6.2.5.1] 🔴 BAD blocking 12 of 12 blocking reloads came back without the requested part [§6.2.5.2] ↳ 12 of those responses carried an Age header: a cache is answering, most likely keyed without the query string, so _HLS_msn and _HLS_part never reach the origin 🟢 OK bad-request _HLS_part without _HLS_msn is refused with 400 [§6.2.5.2] 6 findings: 3 OK, 0 WARN, 3 BAD, 0 ERROR — 19 requests in 6.2s
From llhls-origin, the local test origin in the repository, with the cache fault switched on — no public low-latency HLS stream was reachable to show instead.
A healthy origin, measured
🟢 OK blocking 21 blocking reloads, each answered with the requested part; wait p50 499ms, p95 504ms 🟢 OK cadence a new part every 500ms (median), PART-TARGET 0.5s — in step with PART-TARGET 🟢 OK edge-age a part reaches a blocked client 302ms after its media ends (p50; p95 305ms) 🟢 OK latency a player starting at the recommended hold-back plays 1.80s behind the media's wall clock
In GitHub Actions
Gate a deploy, or watch an origin on a schedule. The report lands in the job summary as a table; exit-on: bad fails the job when the origin breaks a MUST.
- uses: Allan-Nava/latencyprobe@v0.2.0 with: url: https://cdn.example/live/master.m3u8 duration: 60s exit-on: bad
A composite action: it installs the release binary for the runner, verified against checksums.txt — Linux and macOS runners, no image to pull. Inputs and outputs are in the README.
What it checks
Every finding names the requirement it comes from in draft-pantos-hls-rfc8216bis-22. A MUST the server breaks is BAD; a SHOULD, or the Appendix B.1 low-latency server profile, is WARN.
| Check | What latencyprobe does | Rule |
|---|---|---|
blocking | Follows the live edge with _HLS_msn / _HLS_part requests, as a player does. A response without the requested part means the origin is not blocking — or a cache ignores the query string, which the Age header gives away. | §6.2.5.2 |
delta | _HLS_skip=YES must return a delta update past the Skip Boundary, skip only beyond it, and line SKIPPED-SEGMENTS up with the full playlist. | §6.2.5.1, §4.4.5.2 |
rendition-report | Each report against the rendition it describes, fetched straight after: not ahead of it, not a segment behind; one per other rendition. | §4.4.5.4, B.1 |
bad-request | _HLS_part without _HLS_msn must get 400; an _HLS_msn far past the edge should get an immediate 400, not a hang. | §6.2.5.2 |
server-control | HOLD-BACK, PART-HOLD-BACK and CAN-SKIP-UNTIL against the minimums the spec states. | §4.4.3.8 |
cadence | How often a new part actually arrives, against PART-TARGET. | |
edge-age | How long after its media ends, by EXT-X-PROGRAM-DATE-TIME, a part reaches a blocked client. | |
latency | What a player at the recommended hold-back actually plays behind wall clock. |
Limits, stated
Clocks
edge-age compares the packager's clock with yours. latencyprobe warns when the server's Date is more than 1.5 s off; inside that, run it from an NTP-synced machine.
One vantage point
It measures the path from where it runs. A CDN edge near you can behave differently from one elsewhere — run it from where your viewers are.
Playlists, not media
Whether the parts contain what the playlist says is a different question. segcheck downloads the segments and parts and answers it.
Try it without an origin
$ go run ./cmd/llhls-origin --stale-cache --age # a CDN keyed without the query string $ go run ./cmd/llhls-origin --lag 300ms # each part listed 300 ms late $ latencyprobe http://127.0.0.1:<port>/master.m3u8
The same origin runs in the tests, where each fault is switched on and the finding it should cause is asserted.