What the latency actually is
Every remote desktop product claims to be fast. Almost none of them say what they measured, on what hardware, with what left out — which makes the claims unusable, including ours. So this page publishes the number, the machine, the method, the failure modes, and the instrument itself, as a program you can point at your own stack.
The measurement
A probe inside the streamed desktop draws a 12-bit timecode of the machine's monotonic clock, redrawn 240 times a second. An observer on the client machine reads that timecode out of the client's window and subtracts its own clock. What comes out is draw-to-visible.
Included:the probe's draw and commit, the streamed desktop's compositor, the capture, the hardware encode, transport and pacing, the jitter buffer, the hardware decode, and the client's present into its framebuffer.
Excluded:the client monitor's scanout — so this is glass-to-framebuffer, not glass-to-photons — and any input path. Nothing here measures how long a keystroke takes to come back.
Why not our own internal number
Our host agent already timestamps every frame from damage to encode submit, and that number is both smaller and better-looking. It is also useless to you: it stops at the encoder, it comes from inside our own compositor fork, and you cannot reproduce it on your stack. A benchmark that only its author can run is marketing. This one needs nothing but an X11 client inside the streamed desktop and a window on the measuring machine.
Why the arithmetic is allowed
Subtracting one machine's clock from another's is meaningless unless they are the same clock. A container shares its host kernel's monotonic clock unless a time namespace says otherwise, so the measurement asserts that per run — from /proc/1/timens_offsets, which is authoritative, rather than from the inode of /proc/1/ns/time, which produces false negatives on perfectly healthy containers. Across two physical machines the assumption does not hold, and this instrument does not apply.
The results
One session per codec, all three running concurrently on one GPU, arms interleaved so that drift in the machine cannot be charged to whichever arm ran last. Every cell either produces a number or is reported invalid; none are quietly dropped.
| arm | glass-to-glass latency | per-cycle | cells |
|---|---|---|---|
| h264 static desktop | 61.5 ms | 61, 61.5, 69, 57.5, 61.5, 62.5, 67.5, 59.5, 64, 58.5 | 10/10 valid |
| h264 full motion | 69.8 ms | 73, 74, 60.5, 72, 71, 68.5, 76.5, 64, 62.5, 64.5 | 10/10 valid |
| h265 static desktop | 62.5 ms | 75, 61, 59.5, 73, 63, 62, 56, 63.5, 59, 66 | 10/10 valid |
| h265 full motion | 64.5 ms | 61.5, 62.5, 70, 65, 59, 64, 78, 71, 70, 56.5 | 10/10 valid |
| av1 static desktop | 63.8 ms | 68, 62, 63, 67, 57, 64.5, 67.5, 70, 62, 58 | 10/10 valid |
| av1 full motion | 67.8 ms | 69, 66.5, 62, 74, 73.5, 63, 69.5, 63.5, 72.5, 62.5 | 10/10 valid |
ci-nvidia-vm · NVIDIA GeForce RTX 5060 Ti, driver 580.173.02 · x11/GNOME Shell 46.0 · HDMI-0 1920×1080 @60 Hz · 10 cycles × 30 samples per arm · 60/60 cells valid
The workload separates and the codec does not. A screen full of moving pixels costs about four milliseconds over a still desktop. Which hardware codec carries it does not move the number: on a static desktop H.264, H.265 and AV1 land within a millisecond of each other. If you were expecting the codec choice to be the interesting variable, it is not.
Sessions per GPU
The same measurement at one, two, four and eight concurrent sessions, every session streaming a fully moving 1080p60 desktop with the bitrate pinned at 20 Mbit/s. CPU and bandwidth come from each container's own cgroup and network namespace, so no measuring apparatus is included in them.
| sessions | latency | CPU total | CPU per session | egress total | encoder | decoder |
|---|---|---|---|---|---|---|
| 1 session | 47.5 ms | 0.18 cores | 0.18 cores | 22 Mbit/s | 6% | 2% |
| 2 sessions | 63 ms | 0.43 cores | 0.21 cores | 43 Mbit/s | 11% | 5% |
| 4 sessions | 74.5 ms | 1.18 cores | 0.30 cores | 86 Mbit/s | 24% | 10% |
| 8 sessions | 74.5 ms | 2.80 cores | 0.35 cores | 170 Mbit/s | 46% | 19% |
ci-nvidia-vm · NVIDIA GeForce RTX 5060 Ti, driver 580.173.02 · x11/GNOME Shell 46.0 · HDMI-0 1920×1080 @60 Hz · 3 cycles × 30 samples per arm · 12/12 cells valid
The encoder is not what runs out. Eight full-motion sessions leave the hardware encoder under half busy, and the per-session cost is flat enough that saturation sits somewhere past sixteen. What climbs is CPU per session — from about a fifth of a core alone to a third with eight neighbours, for identical work — because the compositor pays for the company it keeps.
What is wrong with this rig, in our own words
- It is one machine. The clients decode on the same GPU that encodes, which a real deployment never does. That contention inflates our own latency, so the numbers above are a ceiling rather than a best case — and it is why the decoder column is reported separately and should be ignored when you are sizing a server.
- It is a virtual machine with the GPU passed through, on consumer hardware. That is closer to a self-hosted deployment than a desktop would be, but it is not a datacenter card.
- Bandwidth is capped, not chosen. Under full motion every codec saturates the 20 Mbit/s cap, so those figures describe the cap. The static-desktop figures are the ones that describe the encoder.
- The observer was checked against itself.Sampling at a tenth of the campaign's rate, eight rounds alternating, moves the number by 0.4 ms — well inside the run-to-run spread. That excludes the instrument distorting the result by more than about five milliseconds, not by less. A cell whose observer became expensive fails its own gate rather than reporting a number.
- Three campaigns have been retracted during this work, each time because the instrument was measuring itself or the machine underneath it. The retractions are kept in the repository next to the results.
Why a hardware video encoder is a different architecture
The comparison worth making is not against a product but against a category. The remote framebuffer protocol (RFC 6143) that the VNC family implements is, by construction, a stream of rectangle updates: the server sends regions of the framebuffer that changed, encoded individually. There are no reference frames and no inter-frame prediction in the protocol's model, so work scales with the area that changed, and in the common implementations that work is done on the CPU.
A hardware video encoder inverts both properties. Cost scales with how much the picture differs rather than how much of it moved, and the work happens on a fixed-function block that is not competing with the applications for CPU. The per-session CPU figures above are what that difference looks like at eight sessions.
We publish no performance number for any named competitor. Several vendors' licence terms forbid publishing benchmark results of their software, and a comparison you cannot reproduce is worth very little anyway. Here is the instrument instead.
Measure your own
The bundle below is generated from the same files that produced every number on this page, and its checksums are recorded inside the run records themselves.
- README — the metric, its exclusions, the validity gate
- g2g_probe.c and Makefile — the in-desktop probe
- run.py, client_probe.py, g2g_measure.py, clock_gate.py — the observer
- sha256sums.txt
Raw run records, exactly as the harness wrote them: latency and density. Every table on this page is rendered from those two files; no number here is typed by hand.
What we do not claim
- That your latency will be this. Yours includes a network we did not measure.
- That this is input-to-photon. It is draw-to-framebuffer, which is smaller than what a user feels by one monitor's scanout and one input path.
- That eight sessions per GPU is a supported product limit. It is what fit on one consumer card with the encoder still half idle.
- That any of this measures a competitor. It does not, and we say so above.
If you run it against your own stack and get something that contradicts this page, we would rather hear it than not: the docs explain how to reach us.