On a clean LAN, WebRTC put a video frame on screen 40 ms after the encoder stamped it (MediaMTX v1.21.3, benchmarked October 2026 on Ubuntu 26.04 and reviewed for this November 2026 update). LL-HLS from the same server took 772 ms, and plain HLS with 6-second segments took 20.6 s. That is the WebRTC vs LL-HLS question in one line: a factor of 19, not the vague “sub-second versus a few seconds” that widely cited comparison pages quote without a test behind it.
You can rebuild the source, server and network shaping from the commands shown. An ffmpeg source burns a millisecond clock into every frame and pushes RTMP into one MediaMTX server. Chrome plays it back over WebRTC, LL-HLS and standard HLS on the source VM, so one clock stamps and reads each frame. Every path was timed on the LAN and again under tc netem delay and loss, since a LAN figure is only a lab floor. The last sections cover RTMP and which protocol fits live-dealer tables, in-play betting, auctions and sports.
WebRTC vs LL-HLS vs HLS: the measured latency
Every cell is the median of 20 frames. It runs from the moment ffmpeg drew the timestamp to the moment Chrome scheduled that frame for display. One stream fed every path: 1280×720 at 30 fps, H.264 baseline at 2.5 Mbit/s, one keyframe per second.
| Delivery path | LAN (lab floor) | 80 ms RTT | 80 ms RTT + 1% loss | 200 ms RTT + 3% loss |
|---|---|---|---|---|
| WebRTC (WHEP) | 40 ms | 87 ms | 84 ms (50 to 84) | 247 ms (129 to 330) |
| LL-HLS, 200 ms parts (MediaMTX default) | 772 ms | 1.14 s | 5.0 s (4.8 to 7.7) | 4.3 s (2.9 to 7.7) |
| LL-HLS, 1 s parts (Apple’s recommendation) | 2.57 s | 2.61 s | 5.7 s (2.6 to 6.8) | not run |
| HLS fMP4, 1 s segments | 3.10 s | 3.40 s | 6.9 s (4.7 to 8.5) | 6.1 s (5.8 to 8.8) |
| HLS MPEG-TS, 6 s segments | 20.6 s | 19.7 s | 26.8 s (25.0 to 30.0) | 44.7 s (24.8 to 48.1) |
| RTMP pull with ffmpeg (no browser) | 8 ms | not run | not run | not run |
On a clean link, each path lands where its buffering model puts it. WebRTC adds the one-way network delay and little else: 40 ms on the LAN became 87 ms with 40 ms added in each direction. LL-HLS holds about three 200 ms parts plus playlist round trips. So the same 80 ms of RTT cost it 363 ms, against the 47 ms WebRTC paid. Classic HLS buffers whole segments, and the 6-second variant never dropped under 19.7 s. Inside one run, the spread was 1 to 2 ms for every HLS row on the LAN and at most 35 ms at 80 ms RTT. These are stable numbers, not lucky frames.
Loss is where the protocols split apart. At 1% loss each way, every HLS variant began to drift: LL-HLS moved from 4.8 s to 7.7 s inside a single 20-sample run. WebRTC measured an 84 ms median at 1% loss (50 to 84 ms) and rose to 247 ms at 3% loss, still a third of the best HLS figure on a clean LAN. The chart puts all 19 runs on one log axis. Each marker is a median, and its line runs from the fastest to the slowest frame, so a long line means the latency moved while we watched:

Before you quote two of these rows, read the caveat. The 6-second HLS row reads 19.7 s at 80 ms RTT against 20.6 s on the LAN, which looks backwards. The network did not cause it. Each run joined the stream at a different point inside a 6-second segment, and that alone moves the result by up to one segment. The loss columns also come from one run per cell, so their medians give the order of magnitude and the min-to-max range is the real finding.
Is RTMP low latency?
RTMP itself adds almost nothing, but no browser can play it, so it has no viewer latency to compare. We pulled the stream back out of MediaMTX with ffmpeg and timed the decoded frames: 8 ms median on the LAN (6 to 10 ms over 20 frames). That covers the encoder, the server, two TCP hops and the decoder, with no player buffer at all. Browsers lost their RTMP player when Adobe ended Flash Player support on December 31, 2020. RTMP is now the first mile: OBS, hardware encoders and ffmpeg push it to an ingest server, which repackages it for viewers.
So “RTMP vs WebRTC” is the wrong pairing. What you actually choose is the protocol that leaves the server toward the viewer, and the RTMP leg sets a floor under all of them. On a long or lossy ingest path, its TCP stalls show up in every output. Some teams move ingest to SRT or to WebRTC itself (WHIP, published as RFC 9725 in March 2025) for that reason. For a camera feeding OBS on the same network as the server, RTMP ingest is fine. A capture card that works on Linux plus OBS Studio on Ubuntu is the usual way to get there.
Step 1: Build the test rig
Two Ubuntu 26.04 VMs on one Proxmox bridge make up the rig. perf-a runs only MediaMTX. perf-b runs both the ffmpeg source and the Chrome player, so the clock that stamps each frame also reads it back. No NTP error enters the result:

Each part has one job, and its version matters, because latency defaults change between releases:
- Server (perf-a, 10.0.1.50): the MediaMTX static binary, unpacked into
~/mtxfrom the release tarball, with RTMP on 1935, HLS and LL-HLS on 8888, WebRTC signalling on 8889 and WebRTC media on UDP 8189. - Source (perf-b, 10.0.1.51): ffmpeg 8.0.1 from the Ubuntu archive, encoding the
testsrc2pattern with x264veryfastandzerolatency, so the encoder holds no lookahead frames. - Player (perf-b): Google Chrome 156 driven by Playwright, loading the MediaMTX player pages, which use WHEP for WebRTC and the bundled hls.js 1.7.3 for HLS.
- Reader: Tesseract 5.5.0, reading the burned-in clock from a crop of each captured frame.
A burned-in timestamp keeps the test honest. The ffmpeg drawtext filter expands %{localtime} with a %3N extension for milliseconds. It is evaluated when the filter processes the frame, just before the encoder sees it. Its escaping does not survive shell quoting well, so keep the filter in a file on perf-b:
vim ~/tc.txt
Paste it exactly as it ran in the lab, triple backslashes included:
drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf:text='%{localtime\:%H\\\:%M\\\:%S.%3N}':fontsize=96:fontcolor=white:box=1:boxcolor=black@1:boxborderw=16:x=40:y=40
Next, the source reads the test pattern in real time, burns the clock in and pushes RTMP to perf-a. The -/filter_complex form loads the option value from that file. TZ=UTC keeps the stamp and the browser clock in one zone, and the fixed 30-frame GOP gives every HLS variant a keyframe each second to cut on:
TZ=UTC ffmpeg -hide_banner -loglevel warning -re -f lavfi -i testsrc2=size=1280x720:rate=30 \
-/filter_complex "$HOME/tc.txt" \
-c:v libx264 -preset veryfast -tune zerolatency -profile:v baseline -pix_fmt yuv420p \
-g 30 -keyint_min 30 -sc_threshold 0 -b:v 2500k -maxrate 2500k -bufsize 1250k \
-f flv rtmp://10.0.1.50:1935/live
MediaMTX logs stream is available and online, 1 track (H264) as soon as the RTMP session opens. The same stream is then readable at http://10.0.1.50:8889/live (WebRTC player page) and http://10.0.1.50:8888/live (HLS player page), with no per-protocol configuration.
Step 2: Check what MediaMTX ships for HLS
MediaMTX serves LL-HLS by default, which surprises anyone who expects plain HLS from a fresh install. The defaults live in the shipped mediamtx.yml, and a grep pulls out the keys that decide HLS latency:
grep -nE "^(hlsVariant|hlsSegmentCount|hlsSegmentDuration|hlsPartDuration|hlsAlwaysRemux):" ~/mtx/mediamtx.yml
On v1.21.3, the file sets the Low-Latency variant with 1-second segments and 200 ms parts:
336:hlsAlwaysRemux: false
341:hlsVariant: lowLatency
345:hlsSegmentCount: 7
351:hlsSegmentDuration: 1s
357:hlsPartDuration: 200ms
Comments next to those keys explain most of the results table. A player “usually puts 3 segments in a buffer” for plain HLS and “3 parts” for LL-HLS, and the number of kept segments does not affect latency. Three 200 ms parts plus playlist requests is close to the measured 772 ms. Three 1-second segments plus the one being written is close to 3.1 s.
Plain HLS comes from the other two values of hlsVariant. We switched between them and changed the segment length in the config, then restarted the server, because these settings are global:
fmp4: fragmented MP4 segments, run here with 1-second segments.mpegts: MPEG-TS segments for maximum compatibility, run here with 6-second segments, the target Apple’s authoring spec sets for plain HLS.
Both cut on the 1-second keyframe interval, so segment length is the only knob that differs from the LL-HLS run.
A 200 ms part is shorter than the Apple guidance. The HLS authoring specification recommends a 1-second part target. It also asks for a part at least three times the 95th-percentile round trip, which a 200 ms part breaks on any path above about 67 ms of RTT. Following Apple cost 2.57 s on the LAN instead of 772 ms. For a browser audience on hls.js, keep the default. The Apple numbers matter when Safari and iOS are your main players, and the comment on hlsEncryption says those devices also need HTTPS for LL-HLS to work.
One more default moves the result more than any part size. With hlsAlwaysRemux: false, MediaMTX starts the HLS muxer only when the first viewer asks, so that viewer joins at the live edge of a short playlist. Our first round ran that way and measured 622 ms for LL-HLS and 6.15 s for 6-second HLS. Later viewers join a full playlist and sit further back. Every figure in the results table comes from the second round, with hlsAlwaysRemux: true and the stream running before each player connected. A viewer of an established stream gets that.
Step 3: Measure glass to glass on one screen
Glass-to-glass latency runs from light entering the camera to light leaving the screen in front of the viewer. A test pattern has no camera, so our span starts when ffmpeg stamps the frame and ends when Chrome schedules it for display. Sensor exposure, capture-card buffering and monitor scan-out fall outside it. On real hardware they add one or two frame times (33 to 67 ms at 30 fps). Everything that differs between protocols sits inside the measured span: encode, ingest, repackaging, network, player buffer and decode.
Most people start with a clock and the player on one screen, photographed together. We tried that first, with both players and a UTC clock in one Chrome page:

Read naively, that capture says WebRTC is 25 ms in the future (49.545 against a 49.520 clock) and LL-HLS is 709 ms behind. The clock is the problem, not the video. A page clock is painted on the frame schedule of the browser, and here it ran about 65 ms behind the composited video. Both players carry the same error, which is why their gap (734 ms) still matches the table. A camera aimed at a monitor has the same kind of error from refresh and shutter timing, so a one-screen photo is good for a demonstration and weak for a number.
For the table we used the frame clock of the browser instead. The script asks the video element for its next frame with requestVideoFrameCallback, which reports when that frame will be displayed, then copies the frame to a canvas and passes the timestamp crop to Tesseract. The method, as it ran:
- Sample count: 20 frames per run, one per second, after an 8-second settle once playback started.
- Display time:
performance.timeOriginplus theexpectedDisplayTimeof the callback, so the moment the frame is shown, not the moment the script ran. - Frame time: the OCR result of the burned-in
HH:MM:SS.mmm, rejected unless it parses fully. - Resolution: the stamp changes every 33 ms, so one sample can be off by a frame; the median of 20 removes most of that.
A sanity check comes free with the rig. The ffmpeg RTMP pull (8 ms) is the shortest path through the same encoder and server. WebRTC on the LAN (40 ms) sits 32 ms above it, about one frame of jitter buffer plus decode and display. If your own rig shows WebRTC below the raw pull, the stamp and the reader are not using the same clock.
Step 4: Add delay and packet loss with tc netem
Shaping the whole interface would also slow the RTMP ingest and the SSH session that drives the test, so netem only catches the delivery ports. A prio qdisc sends all traffic to band 2 by default, and three u32 filters move the HLS, WHEP and WebRTC media ports into band 3, where netem sits. The server side of the 80 ms RTT plus 1% loss profile:
sudo tc qdisc add dev eth0 root handle 1: prio bands 3 priomap 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
sudo tc qdisc add dev eth0 parent 1:3 handle 30: netem delay 40ms loss 1%
sudo tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip sport 8888 0xffff flowid 1:3
sudo tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip sport 8889 0xffff flowid 1:3
sudo tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip sport 8189 0xffff flowid 1:3
Run the same five commands on perf-b with dport in place of sport, so requests and acknowledgements are delayed too and the round trip is real. With the profile on, tc qdisc show dev eth0 lists the netem line as delay 40ms loss 1%. ICMP and SSH are not on a shaped port, so ping will not show the added delay; time an HTTP request to port 8888 instead. We ran four profiles: none, 40 ms each way, 40 ms each way with 1% loss, and 100 ms each way with 3% loss. sudo tc qdisc del dev eth0 root on both VMs removes the shaping.
Step 5: Read why HLS drifts under loss
HLS did not just get slower under 1% loss, it kept getting slower: the 1-second fMP4 run climbed from 4.7 s to 8.5 s, and 6-second HLS at 3% loss swung between 24.8 s and 48.1 s. A player stays live only if it downloads each segment faster than the segment plays. Timing one 6-second MPEG-TS segment (about 1.94 MB at 2.5 Mbit/s) with curl under each profile shows where that breaks:
| Profile | Download time, three tries | Share of the 6 s it must beat |
|---|---|---|
| LAN | 0.004 s, 0.004 s, 0.004 s | under 0.1% |
| 80 ms RTT, no loss | 0.73 s, 0.72 s, 0.72 s | 12% |
| 80 ms RTT + 1% loss | 5.59 s, 5.32 s, 1.61 s | 27% to 93% |
With loss on the path, one TCP connection moved the segment at 347 to 364 KB/s on two of three tries, barely above the 312 KB/s the stream needs. One more retransmission pushes a download past real time, the player stalls, and hls.js claws the time back only when the network allows it (the MediaMTX page sets maxLiveSyncPlaybackRate: 1.5). Shorter parts do not fix this: the 1-second-part LL-HLS run drifted from 2.6 s to 6.8 s just like the default. The fixes are a lower bitrate rung for lossy viewers, an ABR ladder, or a protocol that does not need every byte.
WebRTC is that protocol. Its media runs over UDP, so a lost packet does not hold up the ones behind it, and the 1% loss run stayed between 50 and 84 ms. At 3% loss the median rose to 247 ms, with a 129 to 330 ms spread as the receiver buffered more. We did not log whether Chrome repaired each loss or concealed it, so the claim here covers latency, not picture quality. For an audience on mobile or congested links, that difference matters more than the LAN floor.
Step 6: Match the protocol to live dealer, betting, auctions and sports
Sub-second delivery pays off when the viewer acts on what they see while someone else acts on a faster feed. Live-dealer tables and in-play markets are the clearest cases: a stream seconds behind the official data feed lets fast-feed users act on outcomes slow-feed users have not seen. The tech stack behind big-match betting covers that data side, and settlement integrity is a separate problem that provably fair game design addresses.
| Use case | What delay breaks | Viewer protocol | Closest measurement here |
|---|---|---|---|
| Live-dealer tables | The betting window has to match the dealer the player sees | WebRTC | 40 ms LAN, 87 ms at 80 ms RTT |
| In-play betting | The video must not trail the odds feed by seconds | WebRTC, or LL-HLS with a fixed, known delay | 772 ms to 1.14 s LL-HLS on clean links |
| Live auctions | Bidders react to the call from the auctioneer | WebRTC | 84 ms at 1% loss |
| Sports broadcast | Push alerts and social posts spoil the play first | LL-HLS on a CDN | 0.77 to 1.14 s clean, drifts to 5 s with 1% loss |
| Lectures, webinars, VOD-like events | Little; a few seconds goes unnoticed | HLS with 1 to 2 s segments | 3.1 s LAN |
For in-play markets, WebRTC is not the only defensible answer. An operator can run LL-HLS and hold bet acceptance back by the same known delay, as long as that delay is stable. Our loss runs show it is not stable on a bad link, which pushes lossy regions toward WebRTC. Mass sports audiences fit LL-HLS, where the CDN does the fan-out and a second of delay is the accepted cost.
How scale and load balancing change the choice
Count servers and the latency ranking reverses. Every WebRTC viewer is a stateful, encrypted UDP session that a media server or SFU must feed. Capacity grows by adding SFU nodes and cascading them across regions. HLS and LL-HLS viewers make plain HTTP GETs for the same playlists and segments, which any CDN caches, so one origin can feed a very large audience once CDN edge caching and origin shield collapse the repeat requests. WebRTC buys about 700 ms over LL-HLS and pays for it in server sessions per viewer.
Load balancing differs too. HLS sits behind ordinary HTTP balancers and CDNs, with the patterns in load balancing for high-traffic websites. WebRTC needs the signalling (the WHEP POST) and the UDP media to land on the same SFU node, which means session-aware routing and public UDP ports, not a round-robin pool. LL-HLS adds a CDN requirement of its own: playlist requests block until the next part exists, so the CDN must pass them through promptly instead of serving a stale cached playlist. Many platforms run both: WebRTC for the people who act and LL-HLS for the audience that watches. The WebRTC fundamentals explain the ICE, DTLS and SRTP stack that makes each viewer session heavier than an HTTP request.
Lab snags that skew the numbers
Two effects in this lab skewed a number until we understood them, and both would trip a rerun the same way.
HTTP/1.1 302 Found with Location: /live/index.m3u8?cookieCheck=1
A bare curl of the playlist on MediaMTX v1.21.3 returns an empty body, because the server first answers with a 302 and a cookieCheck=1 cookie. Browsers follow it without help. For scripted segment timing, add -L and a cookie jar (-c /tmp/cj -b /tmp/cj), otherwise every download time you record is the time of an empty response.
LL-HLS reads 622 ms for one viewer and 772 ms for the next
You are seeing the hlsAlwaysRemux effect from Step 2. The first reader starts the muxer and joins a short playlist, while later readers join a full one. For 6-second HLS the jump was from 6.15 s to 20.6 s. Set hlsAlwaysRemux: true and let the stream run before measuring, or the benchmark reports the luck of the first viewer.
Tools in this space
These servers and services take an ingest and deliver it over WebRTC, LL-HLS or both. Only MediaMTX was measured here, so the list is alphabetical and makes no ranking claim:
- Ant Media Server: a media server whose repository lists WebRTC at about 0.5 s alongside SRT, RTMP, HLS and CMAF.
- Cloudflare Stream: a managed service whose WebRTC mode documents sub-second live streaming and playback over WHIP and WHEP.
- Janus: the Meetecho WebRTC server (GPL-3.0), a general-purpose gateway that teams extend for streaming and conferencing.
- LiveKit: an open-source WebRTC SFU and real-time stack (Apache-2.0).
- MediaMTX: the zero-dependency server used in this test (MIT), routing SRT, WebRTC, RTSP, RTMP and HLS; it also appears in our Frigate NVR setup as a test camera source.
- OvenMediaEngine: an AGPL-3.0 streaming server built around sub-second delivery, with WebRTC and LL-HLS outputs.
- SRS (Simple Realtime Server): an MIT-licensed server covering RTMP, WebRTC, HLS, HTTP-FLV, SRT and DASH.
- Wowza Streaming Engine: a commercial, self-hosted streaming server that ingests and repackages live streams for delivery.
Any of them can replace MediaMTX in the rig, since the source and the measuring player only need an RTMP ingest URL and a WHEP or HLS playback URL. If you want to check a vendor latency claim, that takes an afternoon.
Sources
Every latency, download time and default above comes from our lab run on 30 October 2026 (UTC) unless a source below is named for it. The same frame reader, RTMP pull timer and netem helper ran unchanged for every run:
- MediaMTX v1.21.3 release and its shipped configuration: GitHub release and mediamtx.yml.
- Apple part and segment guidance: HLS authoring specification for Apple devices, sections 7.5 and 14.2.
- hls.js
lowLatencyModeandliveSyncDurationCountdefaults: hls.js API documentation. - The
%[1-6]Nfractional-second extension oflocaltimeand the-/optionfile syntax: FFmpeg drawtext documentation and the ffmpeg manual. - WHIP: RFC 9725. WHEP: draft-ietf-wish-whep, still an Internet-Draft.
- Flash Player end of support on December 31, 2020: Adobe.
- Tool descriptions: the site or repository of each project (Ant Media, Cloudflare Stream WebRTC, Janus, LiveKit, MediaMTX, OvenMediaEngine, SRS, Wowza), checked October 2026.