Skip to content

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.

A realm exposes two things worth watching in dev — its projected state + sync health, and its traffic (the number that becomes your Durable Object bill). This page is the realm-specific wiring for both. The window that renders them, the server host, the topology band, and the relay are the same across every plane and live in the Devtools section — this page links there rather than repeating it.

createNiceDevtools() builds the shared devtools cores and streams them; instrument each connectRealm with the tap it hands back. The panel that renders it lives in the devtools window, never in your page.

src/devtools.ts
import { createNiceDevtools } from "@nice-code/devtools";
export const devtools = createNiceDevtools({ topologyId: "my-app", runtime });
gameClient.ts
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 — the live projected store, plus status / version / pending / avatar identity.
  • Timeline — 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 — decoded realm frames per kind (hydrate, patches, write, ack, batch, …), counts + bytes over rolling windows.

Construct the underlying RealmDevtoolsCore (@nice-code/realm/devtools/browser) yourself only if you aren’t using createNiceDevtools. subscribeDiagnostics(listener) on the handle is the post-hoc, multi-listener complement of the onDiagnostic connect option — handy for tests and ad-hoc telemetry (and one subscriber satisfies the unwired-diagnostics console floor).

The realm panel’s Traffic tab shows decoded realm payloads. The true wire cost — encrypted frames, envelopes, handshakes, keepalives — is measured one layer down, at the carrier, with wireTap:

client.ts
const connector = connectChannel(runtime, channel, {
// …transports, storage…
wireTap: devtools.wireTap, // ← every carrier frame, true post-encryption byte sizes
});

devtools.wireTap is one traffic core for the whole app, already streamed to the window (its per-lane, per-client breakdown, sparklines, and largest-frame list are documented on Devtools & Traffic in the window). The realm-specific reading:

  • egressMessages is your billing unit. Each outgoing WebSocket message is a Cloudflare request-count unit; the byte totals are your bandwidth.
  • The realm lane vs. the panel’s decoded totals is the transport overhead (encryption + framing + batch envelope) per payload byte.
  • egressCoalesceRatio (coalesce ×N) is “how many realm frames rode each billable message” — the number to watch when tuning broadcastCoalesceMs and confirming the batch envelope is working.

With no devtools wired at all, the realm engine exposes its own counters — scrape them from a dev-gated route:

MatchDO.ts
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 contribute that same data to a server devtools host (console summaries or a live window feed), the realm scopes are realmEngineScope(engine, "demo_board") (traffic totals) and serverWireTrafficScope(engine) (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:

server.ts
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();

A DO can’t dial out to a devtools window (an open outbound socket pins it awake) and can’t run the console sink’s timer across hibernation — so the window dials in instead, riding the DO’s ordinary WebSocket as a token-gated protocol via createNiceDurableObjectDevtools. The full setup, safety model, and the honest hibernation caveat are in Observing Backends → a Cloudflare Durable Object.