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.
| # | Limit | Design consequence |
|---|---|---|
| 1 | Full state broadcast every tick, roughly 1200 bytes of budget | Keep authoritative state small; derive rendering client-side |
| 2 | Every client receives all state | Hidden information needs client-side handling |
| 3 | No durable persistence across restarts | Days-long async matches are unsupported today |
| 4 | WebTransport only, no WebSocket fallback | Local dev is proven on Chromium |
| 5 | tick runs in a deterministic realm | No wall clocks, raw randomness, or I/O inside tick |
| 6 | Rooms tick at 60Hz while occupied | Turn-based rooms tick too; empty rooms pause |
| 7 | Assorted smaller caps | See the last section |
1. Keep state small
Section titled “1. Keep state small”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.
2. Every client sees everything
Section titled “2. Every client sees everything”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.
3. No durable persistence
Section titled “3. No durable persistence”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
tickskips 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.
4. WebTransport only
Section titled “4. WebTransport only”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.
5. The determinism realm
Section titled “5. The determinism realm”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 write | You 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, hypot | implementations proven bit-identical across engines |
Throws inside tick, allowed in init:
fetch, timers (setTimeoutand friends),crypto.getRandomValues/randomUUID/subtle, file and process APIs. Each throws an error naming the rule and the fix.initruns 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.
6. 60Hz, even for turn-based games
Section titled “6. 60Hz, even for turn-based games”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
tickruns about 60 times a second while players think. Most ticks receive an emptyinputsarray 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 onctx.timeacross 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.
7. Smaller caps you will hit
Section titled “7. Smaller caps you will hit”- 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 receivectx.emitevents on a client, use@siltrun/clientdirectly. 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 devprocess. 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.dataas hostile; clamp and check everything, as the quickstart’s room does.