Skip to content

Limits & Production

What the realm engine bounds for you per connection, the one limit your app must bound itself (state cardinality), and how to keep a realm cheap and safe under load.

A realm host is a shared resource — one Durable Object, many clients, some of them buggy or hostile. So the engine bounds every connection for you, and hands you the one lever it can’t guess: the total size of your state tree. This page is both of those, in one place.

Every realm host enforces per-connection ingress limits. A breach rejects the offending write (surfaced as limits_exceeded) and counts against the connection; maxBreachesBeforeDetach breaches force a detach — a hostile or buggy client cannot hold the host hostage. The client-side coalescer reads maxPatchesPerFrame / maxFrameBytes / writeRatePerSecond off the hydrate and splits over-limit flushes automatically, so a well-behaved app rarely sees a breach at all.

LimitDefaultBounds
maxPatchesPerFrame64Patch ops in one write frame.
maxFrameBytes64 KBEncoded bytes of one inbound realm frame.
maxRecordKeyLength128Dynamic record-key length (also protects the intern dictionary).
maxExpandedLeaves256Leaves one subtree write may expand to.
writeRatePerSecond100Sustained write/intent rate per connection (token bucket).
writeRateBurst200Bucket depth — the burst a connection may spend at once.
maxBreachesBeforeDetach10Limit breaches on one connection before it is forcibly detached.

Override any of them in the serve engine options; the effective values ride the hydrate, so the client splits against the same numbers you set. A bare limits_exceeded that reaches your onMutationRejected (rather than being split away) means a genuinely oversized write — back off, don’t hammer.

What your app must bound: state cardinality

Section titled “What your app must bound: state cardinality”

The engine bounds each frame and each write, but not the total size of a realm’s state — that is application-shaped, so it stays your call. A realm whose state is a record that grows one entry per client (a presence roster, a chat log, a leaderboard) grows without limit unless you cap it. Cap it with a rule — the same place you already authorize writes:

// doc-check: skip — illustrative rule shape; see nice-realm/rules
rules: (r) => [
r.on("roster.*").alter((ctx) =>
Object.keys(ctx.state.roster).length < 500 || err_game.fromId("roster_full"),
),
]

Since bounded schema nodes landed, the declarative form of this cap is t.record(t.id("rosterId"), …, { maxKeys: 500 }) — hashed into the schema, enforced against the candidate next state on every write/intent/server mutation, and checked once over the whole tree at boot. Prefer it for plain entry-count ceilings; keep the rule form for caps that need context a schema can’t see (per-avatar quotas, conditional limits). The same goes for value-size bounds: t.string({ maxLength }) and t.array(item, { maxItems }) bound leaves declaratively.

A total-state maxStateBytes engine knob may arrive later (it remains deferred in the registry); declared per-node bounds cap each dimension, not the product of all of them — for anything attacker-influenced (a key space a client can grow), some cap, declared or ruled, is not optional.

Two hosting choices keep a realm small and inexpensive to run — both covered in full on Serving a Realm, flagged here because they’re the load-and-cost levers:

  • Transient regions (transient: ["presence", ...]) — a branch that never persists and reloads empty on a hibernation wake. Presence, cursors, “who’s typing” — anything you’d re-assert on attach rather than store. See Presence Realms.
  • Ephemeral hosting (persistence: "ephemeral") — the whole realm lives only in memory and cold-starts from genesis; nothing is written to disk. Pair it with assertOnAttach so each client re-owns its entries on a wake.

Bounding state is one line of your pre-flight checklist. The rest — the trust model, payload exposure, devtools in production — is stack-level and lives there and on Identity & Trust. Next in the realm arc: Presence Realms.