Devtools & Traffic
Observing a realm: the browser realm panel, what its decoded traffic means versus the true wire cost, and the egress-message count that maps to your Durable Object bill.
Two things about a realm are worth watching in development: its projected state and sync health, and its traffic, the number that becomes your Durable Object bill. This page covers the realm-specific wiring for both. The window that renders them, the server host, the topology band and the relay are the same for actions and realms and live in the Devtools section, so this page links there rather than repeating it.
The realm panel
Section titled “The realm panel”createNiceDevtools() sets up devtools for the whole app and streams the data out. To observe a
realm, pass each connectRealm the tap it hands back. The panel lives in the
devtools window, never in your page.
import { createNiceDevtools } from "@nice-code/devtools";
export const devtools = createNiceDevtools({ topologyId: "my-app", runtime });const realm = connectRealm(gameRealm, { avatar: { type: "player" }, connection: realmConnection(connector), devtools: devtools.realm("game"), // ← the observation seam (one null-check when omitted)});The panel shows one tab per instrumented realm (status dot, confirmed version, pending-write badge) with three views:
- View shows the live projected store, plus status / version / pending / avatar identity, and two instruments described below: the RTT sparkline and the flush strip.
- Timeline lists every mutation with its outcome (
created → confirmed / rejected, with reject ids and rolled-back paths), every sync-health diagnostic (resync,probe_escalated,link_redialed,reown_missing, …), and status flips. - Traffic shows decoded realm frames per kind (
hydrate,patches,write,ack,batch, …), as counts and bytes over rolling windows.
RTT and the flush strip — reading a client’s shape
Section titled “RTT and the flush strip — reading a client’s shape”Beside the sync-health row, the View tab charts two things that tell you how a client is behaving, as opposed to whether it is connected:
- The RTT sparkline plots
realm.statsover time: the smoothed confirmed round trip, its jitter (mean absolute deviation), and the sample count. It appears once there is something to plot. RTT is measured on a confirmed round trip, so a realm that only ever reads has none, and the panel shows nothing rather than a0 msthat isn’t true. - The flush strip draws one tick per recent projection flush, oldest on the left, coloured by
meta.cause, the same provenancelistenToPatchesreports. Ticks for non-authoritative flushes are dimmed.
What matters is the mix, which is why this is a strip rather than rows in the Timeline: a game client flushes at its tick rate and would bury every discrete event in the log. Read it like this:
| What you see | What it means |
|---|---|
| Mostly optimistic (warm, dimmed) | The client is predicting well ahead of what the server has confirmed: an overlay-heavy client, visible at a glance. |
| Mostly rebase | Server patches are landing while writes are in flight. Normal and expected for any client that writes continuously. |
| Mostly confirm | Server truth arriving with an empty outbox: a client that mostly watches. |
| Hydrate ticks mid-session | Full rebuilds. One at attach is the hydrate. More than that means re-syncs, so check the Timeline for resync. |
| No authoritative ticks at all | Nothing is arriving from the server, whatever the status dot says. |
Both series are bounded rings (maxSamples, default 240). They carry no leaf values, only
timings, a cause and a flag, so they stream unchanged from a
shape-gated remote producer.
Construct the underlying RealmDevtoolsCore (@nice-code/realm/devtools/browser) yourself only if you
aren’t using createNiceDevtools. subscribeDiagnostics(listener) on the handle complements the
onDiagnostic connect option: it can be added after connecting and takes multiple listeners. It is
handy for tests and ad-hoc telemetry (and one subscriber satisfies the unwired-diagnostics console
floor).
Decoded payload vs. true wire cost
Section titled “Decoded payload vs. true wire cost”The realm panel’s Traffic tab shows decoded realm payloads. The true wire cost counts
encrypted frames, envelopes, handshakes and keepalives. It is measured one layer down, at the
carrier, with wireTap:
const connector = connectChannel(runtime, channel, { // …transports, storage… wireTap: devtools.wireTap, // ← every carrier frame, true post-encryption byte sizes});devtools.wireTap measures traffic for the whole app and is already streamed to the window. Its
per-lane and per-client breakdown, sparklines and largest-frame list are documented on
Devtools & Traffic in the window. The realm-specific reading:
egressMessagesis your billing unit. Each outgoing WebSocket message is a Cloudflare request-count unit. The byte totals are your bandwidth.- The
realmlane vs. the panel’s decoded totals gives the transport overhead (encryption, framing and the batch envelope) per payload byte. egressCoalesceRatio(coalesce ×N) is how many realm frames each billable message carried. Watch it when tuningbroadcastCoalesceMsand confirming the batch envelope is working.
Reading the engine directly
Section titled “Reading the engine directly”With no devtools wired, the realm engine still keeps its own counters. Scrape them from a dev-gated route:
if (url.pathname === "/debug/traffic" && env.STAGE !== "production") { return Response.json(this.engine.getTrafficSnapshot()); // { ingressFrames, ingressBytes, egressMessages ← billing unit, egressFrames ← pre-coalescing, // egressBytes, egressCoalesceRatio ← the batch-envelope win, connections: [...] }}To send that same data to a server devtools host (console summaries or a live window feed), use
the realm scopes: realmEngineScope(engine, "demo_board") for traffic totals and
serverWireTrafficScope(engine) for per-frame samples. The host itself (its options, sinks, and how
it streams to a window alongside your action domains) is documented in
Observing Backends:
import { createServerDevtoolsHost } from "@nice-code/devtools-core";import { realmEngineScope, serverWireTrafficScope } from "@nice-code/realm/devtools/server";
createServerDevtoolsHost({ name: "match-api" }) .contribute(realmEngineScope(engine, "demo_board")) .contributeSample(serverWireTrafficScope(engine)) .start();Inside a Durable Object
Section titled “Inside a Durable Object”A DO can’t dial out to a devtools window, because an open outbound socket pins it awake, and it
can’t run the console sink’s timer across hibernation. So the window dials in instead, over the
DO’s ordinary WebSocket as a token-gated protocol, via createNiceDurableObjectDevtools. The full setup,
safety model and hibernation caveat are in Observing Backends → a Cloudflare Durable Object.