Read per-session telemetry.
const url = 'http://localhost:8080/v1/admin/sessions/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/metrics';const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}use reqwest;
#[tokio::main]pub async fn main() { let url = "http://localhost:8080/v1/admin/sessions/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/metrics";
let mut headers = reqwest::header::HeaderMap::new(); headers.insert("Authorization", "Bearer <token>".parse().unwrap());
let client = reqwest::Client::new(); let response = client.get(url) .headers(headers) .send() .await;
let results = response.unwrap() .json::<serde_json::Value>() .await .unwrap();
dbg!(results);}curl --request GET \ --url http://localhost:8080/v1/admin/sessions/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/metrics \ --header 'Authorization: Bearer <token>'Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Responses
Section titled “Responses”OK.
object
One row from GET /v1/admin/sessions/{id}/metrics.
object
Metrics from the node-agent encode pipeline.
object
The governor’s live floor, sent ONLY when the ladder has moved it off the launch floor. Absent means ‘the floor the session launched with’, never ‘unknown’. Uncurated: readable via GET …/metrics, not in the diagnostic lens.
The ABR mode this session is actually running (‘smooth’ | ‘protective’ | ‘off’). Reported rather than inferred: deriving it from setpoint presence was wrong for ‘off’, where rtpgccbwe stays attached. Uncurated (a label, not a series).
The in-session ABR governor’s CURRENT CBR setpoint at drain time — a level, not a window average. Absent when ABR is disarmed.
The agent’s own per-window bottleneck label (‘healthy’ | ‘network_congested’ | ‘encoder_saturated’ | ‘unknown’). SIGNAL-ONLY — it changes no ABR behaviour, and it can never say ‘client_presentation_limited’ because the agent never sees browser data (invariant #1).
Frames emitted by waylanddisplaysrc per second, BEFORE caps normalisation and interpipe. The first stage where a rate can differ from what the app painted.
P50 spacing of consecutive compositor buffer PTS. NOMINAL, NOT REALIZED: PTS come off the shared pipeline clock at the negotiated rate, so this sits at a flat ~16.668 ms at 60 fps however badly the wall-clock cadence jitters. For realized emit spacing read probe_compositor_frame_interval_p95_ms.
P95 of the same NOMINAL PTS spacing. Same trap as the p50: a healthy-looking 16.7 here is not evidence the compositor emitted on time.
Who owns the external size — ‘auto’ (the ladder) or ‘pinned’ (a user/admin PATCH). Rides the size echo, so it is present only while the size is off the launch size.
Whether this session’s encode path can change the external resolution live. Always sent by a capable agent; ABSENT MEANS UNKNOWN, not false.
The raw rtpgccbwe bandwidth estimate BEFORE the governor’s EWMA / deadband / step logic. The delta against abr_setpoint_kbps is the governor’s own smoothing contribution.
Leaky-queue overruns in the window. Host-side loss BEFORE the encoder; distinct from frames_dropped (encoder) and from the client’s frames_dropped (presentation).
P50 time a buffer spent between the interpipe sink and src. Omitted when nothing traversed.
P95 interpipe dwell — where a queue that is filling shows up before drops do.
Deepest the interpipe queue got, in buffers, since the previous drain reset it. A max, so one burst sets it for the whole window — which is the point.
The encoded frame rate the fps rung currently asks for. Present only BELOW the launch rate; only a 120 fps session can ever carry it.
The ladder’s current external-resolution rung index (0 = launch). Present only when non-zero.
The ABR ladder’s current encoder speed-bias rung. Present only when non-zero — absent is ‘baseline quality posture’, never ‘unknown’.
Host-stage latency probe: p50 of compositor src to encoder sink — the pre-encode transit the interpipe hides. Present ONLY when QUASAR_LATENCY_PROBE is on AND that stage sampled this window — absent means not measured, never 0. Observability only: nothing in the control plane may key on it.
Host-stage latency probe: p95 of compositor src to encoder sink — the pre-encode transit the interpipe hides. Present ONLY when QUASAR_LATENCY_PROBE is on AND that stage sampled this window — absent means not measured, never 0. Observability only: nothing in the control plane may key on it.
P95 REALIZED wall-clock spacing of compositor emits — the honest counterpart to the nominal compositor_pts_delta_p95_ms. Probe-gated.
Host-stage latency probe: p50 of encoder src to the pre-SRTP seam, FIFO-paired and guarded at one frame period so a persistent off-by-one cannot inflate every sample. Present ONLY when QUASAR_LATENCY_PROBE is on AND that stage sampled this window — absent means not measured, never 0. Observability only: nothing in the control plane may key on it.
Host-stage latency probe: p95 of encoder src to the pre-SRTP seam, FIFO-paired and guarded at one frame period so a persistent off-by-one cannot inflate every sample. Present ONLY when QUASAR_LATENCY_PROBE is on AND that stage sampled this window — absent means not measured, never 0. Observability only: nothing in the control plane may key on it.
Host-stage latency probe: p50 of payloader to the pre-SRTP seam, keyed on the RTP timestamp rather than on order, so unlike enc_out_to_send it cannot desync and keeps its tail. Present ONLY when QUASAR_LATENCY_PROBE is on AND that stage sampled this window — absent means not measured, never 0. Observability only: nothing in the control plane may key on it.
Host-stage latency probe: p95 of payloader to the pre-SRTP seam, keyed on the RTP timestamp rather than on order, so unlike enc_out_to_send it cannot desync and keeps its tail. Present ONLY when QUASAR_LATENCY_PROBE is on AND that stage sampled this window — absent means not measured, never 0. Observability only: nothing in the control plane may key on it.
Host-stage latency probe: p50 of the compositor’s own PTS-to-emit hold, from Element::current_running_time. Present ONLY when QUASAR_LATENCY_PROBE is on AND that stage sampled this window — absent means not measured, never 0. Observability only: nothing in the control plane may key on it.
Host-stage latency probe: p95 of the compositor’s own PTS-to-emit hold, from Element::current_running_time. Present ONLY when QUASAR_LATENCY_PROBE is on AND that stage sampled this window — absent means not measured, never 0. Observability only: nothing in the control plane may key on it.
Encoder-input PTS that never matched a compositor-emit ring entry — the correlation key stopped surviving to the encoder. Probe-gated.
Encoder-src entries dropped from the S3 pairing for exceeding one frame period of age — the count that says enc_out_to_send’s tail is truncated rather than clean. Probe-gated.
The compositor’s CURRENT app-facing wl_output logical height; see render_width.
The compositor’s CURRENT app-facing wl_output logical width. Present only when it differs from the session’s pinned stream size. With render_height and ui_scale this is the ONLY authoritative readback of live render resolution — the value lives nowhere else.
RTP packet bitrate before WebRTC transport overhead (SRTP, RTX, FEC). Always a little above bitrate_kbps; a large gap is retransmission.
RTP access units completed per second, counted on marker packets — the last host-side rate before the wire.
New buffers committed by the MAPPED FULLSCREEN app top-level per second. Cursor, popup, subsurface and configure-only commits are excluded, so this is the app’s own paint rate, not Wayland traffic.
The session’s CURRENT external (encoded) height; see stream_width.
The session’s CURRENT external (encoded) width, read back from the encode graph’s NEGOTIATED caps — not from the last request. Present only when != the launch size, so absent means ‘at launch size’, never ‘unknown’.
The compositor’s CURRENT fractional-scale preferred_scale. Present only when != 1.0.
Metrics from the browser WebRTC client.
object
Presentation fps derived from the MEAN RVFC interval. DEPRECATED as a headline: at source fps == display Hz a single missed vsync doubles one interval and drags the mean (a healthy 1440p120 session read 88-108 on 2026-08-22). Read present_fps_median. Retained unchanged for series continuity.
Presentation fps from the MEDIAN RVFC interval — the fps a viewer perceives, unmoved by an occasional doubled frame. The number to read.
Median frame-to-frame presentation interval (ms).
Longest presentation interval in the window (ms) — the real stall length, and the source of client.freeze_detected’s gap_ms.
Share (0..1) of the window’s intervals within +/-20% of exactly 2x the median. At source fps == display Hz this is the inherent vsync beat, not stutter; read it beside present_long_frames.
Intervals longer than 2.5x the median — above the doubled band, so genuine stalls rather than the beat. Zero is what makes a beat benign.
Intervals the window actually contained. Below 5 every other present_* key is omitted rather than computed from a fragment.
RVFC captureTime has a current validated sample (0/1); stale/missing/invalid captureTime resets it. Not proof of abs-capture-time RTP negotiation.
Strict abs-capture-time RTP-extension negotiation evidence (0 until SDP/RTP wire proof exists).
CUMULATIVE getStats freezeCount. Its advances drive the client.freeze_detected event; the freeze LENGTH is present_interval_max_ms, never 1000/freezeCount. Stored and taxonomised since P1; declared here 2026-08-23.
The client monitor’s refresh rate, measured from rAF vsync spacing with a MEDIAN (AS10-14). Not RVFC-derived. Stored and taxonomised since AS10-14; declared here 2026-08-23.
DEPRECATED in favour of rvfc_capture_to_display_ms, which names what is actually measured. Unchanged in value and still posted: RTP capture-time to browser present, MEDIAN over a never-drained ring of up to 600 RVFC samples — minutes of history, not this second, and not true glass-to-glass (it excludes app render and client scan-out).
RTP capture-time to browser present (ms), median over a rolling ring of up to 600 RVFC samples. Identical in value to glass_to_glass_ms, which it replaces; both are posted this release so no stored series breaks. Additive, approved by Michael 2026-08-23.
Accepted by the browser ingest allow-list and declared here, but no client has ever posted it — the browser cannot observe host encode time. Kept declared so the ingest contract does not change.
AS10-11: tab visibility at sample time (0/1).
AS10-13 input-pipeline health: the DataChannel hit its backpressure threshold. POSTED EVERY SECOND AND SILENTLY DROPPED — it is not in FilterBrowserMetrics’s allow-list, so it is visible live in the diagnostics panel and exists in no stored sample.
AS10-13 input-pipeline health: DataChannel bufferedAmount — the backpressure reading. POSTED EVERY SECOND AND SILENTLY DROPPED — it is not in FilterBrowserMetrics’s allow-list, so it is visible live in the diagnostics panel and exists in no stored sample.
AS10-13 input-pipeline health: pointer samples coalesced away per second (a subset of input_msg_per_sec). POSTED EVERY SECOND AND SILENTLY DROPPED — it is not in FilterBrowserMetrics’s allow-list, so it is visible live in the diagnostics panel and exists in no stored sample.
AS10-13 input-pipeline health: connected gamepads. POSTED EVERY SECOND AND SILENTLY DROPPED — it is not in FilterBrowserMetrics’s allow-list, so it is visible live in the diagnostics panel and exists in no stored sample.
AS10-13 input-pipeline health: gamepad state messages sent per second. POSTED EVERY SECOND AND SILENTLY DROPPED — it is not in FilterBrowserMetrics’s allow-list, so it is visible live in the diagnostics panel and exists in no stored sample.
AS10-13 input-pipeline health: relative mouse-motion messages sent per second. POSTED EVERY SECOND AND SILENTLY DROPPED — it is not in FilterBrowserMetrics’s allow-list, so it is visible live in the diagnostics panel and exists in no stored sample.
AS10-13 input-pipeline health: input DataChannel messages sent per second. POSTED EVERY SECOND AND SILENTLY DROPPED — it is not in FilterBrowserMetrics’s allow-list, so it is visible live in the diagnostics panel and exists in no stored sample.
AS10-13 input-pipeline health: the input tracer is on (seq + tc stamped). POSTED EVERY SECOND AND SILENTLY DROPPED — it is not in FilterBrowserMetrics’s allow-list, so it is visible live in the diagnostics panel and exists in no stored sample.
Example
{ "items": [ { "source": "agent" } ]}Missing/invalid/expired/revoked token.
object
object
E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, restart_required, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.
Present on restart_required.
Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.
Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.
object
Examplegenerated
{ "error": { "code": "example", "message": "example", "live_sessions": 1, "session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "derived_tiles": [ { "id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "name": "example" } ] }}Authenticated but insufficient role / not the owner (precedes resource lookup).
object
object
E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, restart_required, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.
Present on restart_required.
Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.
Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.
object
Examplegenerated
{ "error": { "code": "example", "message": "example", "live_sessions": 1, "session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "derived_tiles": [ { "id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "name": "example" } ] }}No such resource.
object
object
E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, restart_required, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.
Present on restart_required.
Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.
Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.
object
Examplegenerated
{ "error": { "code": "example", "message": "example", "live_sessions": 1, "session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "derived_tiles": [ { "id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "name": "example" } ] }}