Skip to content

Performance Budgets

Budgets are requirements with numbers. The latency harness measures them continuously; a regression is a failing test. Numbers marked ~ are initial engineering targets to be confirmed against M1 measurements (then this doc goes stable).

Latency (p50 / p95, 1080p30 primary track)

Section titled “Latency (p50 / p95, 1080p30 primary track)”
PathGlass-to-glassInput-to-photon
LAN direct~120 / 200 ms~150 / 250 ms
Internet P2P (STUN)~200 / 350 ms~250 / 400 ms
TURN relay~250 / 450 ms~300 / 500 ms

Terminal echo round-trip: < 150 ms LAN. Pointer motion send rate ≤ 60 Hz coalesced; stale motion dropped, never queued.

These are the numbers the nightly run on known hardware is held to. The per-push CI job measures the same paths but fails only above a loose ceiling, because a shared runner cannot hold a p95 honestly and a flaky gate teaches people to re-run it (docs/15).

What the budgets above have actually measured, on named hardware, with the roadmap entry that recorded each number. This table is what the landing page quotes; a number not here is not on the page. Add a row when a gate measures something new, and replace a row (never average it) when a later run supersedes it.

WhatMeasuredBudgetWhere, when
Camera glass-to-glass, clean LANp50 56 / p95 79 ms120 / 200nightly runner, 46–52 decoded fps, M1 gate 2026-09-25 (docs/17)
Camera glass-to-glass, lossy link82 / 100 ms200 / 350same run
Camera glass-to-glass, through a TURN relay69 / 102 ms250 / 450same run
Terminal interactive round trip11 ms< 150 msM2, 2026-09-27
Desktop input-to-photon, LANp50 49 / p95 73 ms (50 presses, 5 runs; slowest 93)150 / 250mini-PC (Ryzen 7 5700U, GNOME 50, DMA-BUF capture, build c5dba78), measured from the dev machine, both on 5 GHz Wi-Fi, M3 gate review 2026-10-18; replaces 3.L’s p50 52 / p95 59 (2026-10-01, before the local cursor and DMA-BUF)
Desktop: monitor plugged in → new track offered101 ms, no frames dropped on the others—M3 3.4, 2026-10-01
Desktop: killed session → back, unattended51 s—M3 3.L, 2026-10-01
Tunnel: ssh to a shell~130 ms—M4.5 slice 4.5c, 2026-09-26
Tunnel: hash-verified 1 GiB scp via fjarr-connect246–253 Mbps—M4.5 gate 2026-09-27 (docs/27)
Camera beside a saturating tunnel transfer31–33 fps, no lost framesbulk vs interactiveM4.5 slice 4.5c, 2026-09-26
TierResolution/fpsTarget bitrate
Active (operator focused)up to 1920×1080@302.5–5 Mbps adaptive
Inactive thumbnail960×540@5≤ 300 kbps
Disabled—~0 (valve dropped; no renegotiation)

Audio (Opus): 32–64 kbps per track downlink, 24–48 kbps uplink; mouth-to-ear < 250 ms P2P. Stream-class sensor frames (ADR-0018): capability-declared ceiling (e.g. a decimated point cloud ≤ 2 Mbps at 5–10 Hz); frames older than one interval are dropped, never queued.

Adaptive bitrate is a hard requirement (the camera-streamer gap): the encoder target follows congestion feedback (per-peer TWCC into the agent’s own estimator — ADR-0007) between a floor of 250 kbps and the tier target, reacting within ~2 s to loss and recovering within ~10 s. Fixed-CBR-only operation is a spec violation. Because one encoder serves every viewer of a tier, the encoder only follows its viewers within a band (half the tier target and up); a viewer whose link is below the band is moved to the lower tier on its own, so one bad receiver never costs the others more than that band (docs/23).

Re-enabling a track waits on the keyframe rate limit. Requests are throttled to one per second per producer tier and deferred inside that window (docs/23), so a viewer that re-enables a track sooner than that waits the remainder, then the encoder’s own latency, then the hub’s start-at-a-keyframe rule. Measured over 20 rapid enable/disable cycles: worst 757-949 ms on 14 cores with VA-API, and ~2.9 s on a 2-core runner with the software encoder. The throttle is deliberate — it protects the encoder from a viewer toggling in a loop — so this is a floor to design around rather than a regression to chase.

The ~2 s reaction above is the estimator’s, measured from feedback that already carries a low rate. Demoting a viewer that is behind a bad link from the moment it connects takes far longer, because nothing can be judged until transport feedback arrives at all: measured at 11.9–20.4 s after the track was enabled (bad: 15 % loss, 100 ± 40 ms, 1.5 Mbit), and longer again when other viewers share the encoder (slice 4.5b). That is not a budget violation — the 2 s figure governs an established session — but it is the number to design tests and support expectations around, and it is why a joining viewer on a bad link streams poorly for a few seconds before it is moved down. A passthrough track (the camera’s own encoded stream) cannot adapt at the agent: it adapts by tier only when the source offers a lower stream, otherwise not at all, and says so (adaptive: false). In exchange it costs no encoder at all — the robot’s GPU and CPU budgets above are per transcoded track, so a fleet of passthrough cameras is bounded by the network, not by the encoder count.

Robot resource budget (Intel NUC class, one active session + one thumbnail)

Section titled “Robot resource budget (Intel NUC class, one active session + one thumbnail)”
ResourceBudget
CPU (agent total)< 25 % of one core with VA-API encode; capture-copy path < 60 %
GPUencode within iGPU capacity for 2 concurrent 1080p30 encodes
RAMagent RSS < 300 MB steady state
Store-and-forward diskbounded ≤ 200 MB (oldest-first eviction + drop counter)
Heap allocations per frame on the streaming hot path (FrameHub → appsrc), steady stateone metadata-only GstBuffer header per subscriber per frame (the fan-out’s per-subscriber PTS rebase, docs/23) and GStreamer’s own refs — nothing else: no pixel copy, no per-frame std::function, string or container allocation (heaptrack)
Object census after a 200-session soakidentical to baseline; RSS growth < 5 MB

Multi-viewer scales via FrameHub: +1 viewer ≈ +RTP fan-out cost only (no new encode) — verify ≤ 5 % CPU per additional viewer.

Browser viewers are relay-realistic: capacity-plan TURN at ~1 stream = 0.5–5 Mbps depending on activity. Ten concurrent operator views ≈ tens of Mbps through the relay — metered per session (docs/10), priced through (docs/03).

Measured by the browser lab profiling scenarios (initial targets, confirmed at slice 5):

MetricBudget
Main-thread busy while streaming 2 active tracks (60 s)≤ 15 %
Long tasks (> 50 ms) while streaming0 per minute after first frame
INP (dashboard interactions while streaming)≤ 200 ms
JS heap growth per connect/disconnect cycle (100-cycle soak, after GC)≤ 50 KB/cycle, no monotonic listener/node growth
Time to first frame after select-tracks enable (LAN, keyframe requested)≤ 300 ms
Frames dropped on unchanged tracks during a renegotiation0 (frame-stamp counter)

Connection health thresholds {#connection-health-thresholds}

Section titled “Connection health thresholds {#connection-health-thresholds}”

The web client’s health score (docs/21) derives from the selected candidate pair’s RTT and the windowed packet loss across received tracks:

LevelCondition (any)
goodRTT ≤ 300 ms and loss ≤ 5 % and no freeze in the window
degradedRTT > 300 ms, or loss > 5 %, or a freeze ≥ 500 ms in the window
poorRTT > 600 ms, or loss > 15 %, or no frames decoded for a full window while a track is enabled

A level changes only after three consecutive 1 s samples agree (hysteresis). The numbers are initial targets in the same sense as the latency table above.

During a saturating file transfer: interactive video g2g p95 may degrade by at most +50 ms and input-to-photon by +30 ms vs baseline. If a shared SCTP association can’t hold that, bulk moves to a separate PeerConnection (measured decision, M4).

  1. Reduce inactive-tier tracks (fps, then resolution).
  2. Reduce active-tier bitrate toward the floor.
  3. Reduce active fps (30→15) before resolution.
  4. Drop inactive tracks entirely (UI shows “paused — bandwidth”).
  5. Never: silently stall media, queue stale input, or let heartbeats starve (control DC has priority).

Startup: first frame visible < 2 s after session-accept on P2P, < 3 s on relay (keyframe-on-connect required). Hot-plug (docs/08 renegotiation): a newly connected monitor’s first frame < 2 s after the monitors event; zero dropped frames and no re-attach on unchanged tracks during the renegotiation.