Limits

Silt is in alpha, and some limits are load-bearing. Read this page before you commit to a game design; several of these change what is worth building today.

#LimitDesign consequence
1Full state broadcast every tick, roughly 1200 bytes of budgetKeep authoritative state small; derive rendering client-side
2Every client receives all stateHidden information needs client-side handling
3No durable persistence across restartsDays-long async matches are unsupported today
4WebTransport only, no WebSocket fallbackLocal dev is proven on Chromium
5tick runs in a deterministic realmNo wall clocks, raw randomness, or I/O inside tick
6Rooms tick at 60Hz while occupiedTurn-based rooms tick too; empty rooms pause
7Assorted smaller capsSee the last section

The server broadcasts the entire authoritative state to every client, every tick, as a single datagram. There is no diffing and no interest management, and the usable datagram budget is roughly 1200 bytes. At around 40 bytes of JSON per small entity, that is a ceiling of roughly 28 to 30 small entities. State that outgrows the datagram silently stops arriving on that lane.

What to do instead:

  • Store the minimum the server must own to decide truth: positions as plain numbers, short keys, no derived data. For a turn-based game this is easy: a board, hands, whose turn it is.
  • Derive rendering client-side. Interpolation, animation, particles, and layout are all computable from authoritative state and belong in the client.

Every client receives the identical state broadcast. There is no per-player view or server-side filtering, so hidden hands, fog of war, and secret roles are all readable from the wire by anyone who looks.

What to do instead: handle hidden information client-side with a cryptographic protocol such as commit-reveal. Players submit a hash of their secret on the reliable lane, the secret stays on the client, and the reveal plus verification happens in a later tick. Note that crypto.* throws inside tick (see limit 5), so verification either happens on clients or uses a pure-JS hash inside the contract.

What actually holds:

  • A crash of your contract does not lose state. The server keeps a recovery copy of the last good state and restores it. One bad tick never kills the room; a throwing tick skips that tick and the error lands in the dev log.
  • A room restart loses state. The recovery copy lives in memory. Restarting siltrun dev, saving your contract file in dev, and redeploying all start fresh: init() runs again at tick 0.

The consequence: do not build a play-by-mail game expecting the room to hold the match for days. If a match must survive, keep the source of truth outside the room and re-seed a new room from it. init() has the full runtime and may fetch.

The client dials WebTransport and nothing else. There is no WebSocket fallback. A browser without a usable WebTransport datagram API fails loudly, with the missing capability named in the error.

Platform support is not the constraint. WebTransport reached browser Baseline in March 2026, including iOS, and @siltrun/client handles the datagram-writer API differences between engines for you.

The caveat is narrower and local: the dev-certificate flow is proven on Chromium. siltrun dev serves your room over a self-signed certificate, and Chrome and Edge accept the hash the client hands them without ceremony. Other browsers handle that step differently, so test your own local loop in whichever browser you develop in rather than assuming it matches. Deployed rooms use ordinary trusted certificates and have none of this friction.

This also means a room URL is not an HTTP URL. It will not open in a browser tab and curl gets nothing from it. A blank tab is not evidence the room is down; use a connected client or the deploy check page to verify.

Your tick must produce the same state from the same inputs on every replay and every engine. That property is what makes crash recovery and the determinism check possible, and the runtime enforces it by running tick inside a prepared realm.

Swapped, so they work but are deterministic:

You writeYou get
Math.random()a seeded per-tick stream, identical on replay
Date.now(), new Date(), performance.now()simulation time, never wall time
Math.sin, cos, atan2, exp, log, pow, hypotimplementations proven bit-identical across engines

Throws inside tick, allowed in init:

  • fetch, timers (setTimeout and friends), crypto.getRandomValues / randomUUID / subtle, file and process APIs. Each throws an error naming the rule and the fix. init runs once, server-side, with the full runtime, so loading data over the network at room creation is fine.

Throws everywhere: the unproven corners of Math (tan, asin, acos, sinh, and similar), which engines approximate differently. The error suggests a rewrite in terms of the proven set.

Inside tick you have plain computation, the proven Math surface, and the ctx argument: ctx.tick and ctx.time for time, ctx.random() for randomness, ctx.emit(event) for server-to-client events.

The enforcement is a global swap plus the determinism check, and the check is the backstop: siltrun dev replays your room’s tick sequence and reports drift at the exact tick and field. A red check means your room will desync in production, so fix the drift rather than fighting the check.

The tick loop runs at 60Hz while at least one player is connected, whether or not anything is happening. For a turn-based game:

  • Your tick runs about 60 times a second while players think. Most ticks receive an empty inputs array and should return state unchanged, cheaply.
  • Simulation time (ctx.time) advances only while the room is occupied. An empty room pauses, and the next join resumes where it left off. Wall-clock turn timers therefore cannot be built on ctx.time across an empty-room gap.

A slow tick degrades gracefully: the loop coalesces to a lower effective rate and inputs pile up latest-wins. A wedged contract is respawned and restored from the recovery copy.

  • Reliable event queue: 256 per tick interval, drop-oldest. Reliable means ordered and delivered under normal load, without infinite buffering. Turn-based games are nowhere near this; event-spamming clients can be.
  • Server emits do not reach useRoom. To receive ctx.emit events on a client, use @siltrun/client directly. See Connecting a client.
  • Two live clients with the same id fight. Each supersedes the other and auto-reconnect makes them loop. Give every client instance its own id.
  • One room per siltrun dev process. Multiple rooms locally means multiple dev processes on distinct ports.
  • Client input is not validated for you. The server guards packet ordering, and your contract is the validator. Treat ev.data as hostile; clamp and check everything, as the quickstart’s room does.