DOM ENGINEV0.21.3FIZLET

2026-10-31 FIELD NOTES · MULTIPLAYER

Lockstep multiplayer in a few KB
Inputs, not state. Twelve bytes a message.

@fzlt/core/presence is 1,834 bytes gzip. Every peer runs the same fixed-step game from the same seed, and only inputs cross the wire, 12 bytes a message. Our room server is in-house: a Cloudflare Worker with one Durable Object per room, run for our own games. It seats players and relays messages; it never runs the game. It isn't a hosted service, so the Build your own room server guide specs its protocol for yours. This note covers only what has shipped, and how each part is tested.

By Chris Cooley ↗ · · 3.3 KB gz

The numbers

  • 1,834 B

    @fzlt/core/presence, gzip (budget 2,048 B)

  • 12 B

    every message: kind, seat, players, frame, value

  • 2 frames

    default input delay: 33 ms at 60 Hz

  • 30

    frames between state-hash checks

Inputs, not state

Fizlet steps at a fixed rate, and a sim that draws from engine.random and keeps its state in typed arrays replays exactly from a seed. So two browsers don't need to send each other the world. They send what each player pressed, packed into one 32-bit word per frame, and both run the same frames.

Your input for a frame is sent two frames ahead, so on a good link nobody waits. A frame runs only when every player's input for it is in. A missing input stalls the sim (net.stalls counts it); it never guesses, so peers can't drift. Every 30 frames the peers swap a hash of their state, and net.desync names the first frame that differed.

How it is tested

  • Two clients stay byte-identical for 3,000 frames with no latency.
  • They stay identical with delayed, jittered and reordered inputs and unequal frame rates, and with four players at latency 3.
  • A forced desync is caught at the next hash check.
  • Presence adds no allocation: the test counts young-generation GCs over 2 × 200,000 frames of the same sim, with and without it.
  • Through the room server on wrangler dev, locally: two Fizlet engines play 600 lockstep frames through one room, byte-identical.

syncCheck from @fzlt/core/presence/check is the same check for your own game: it runs your sim as lockstep clients over a jittery in-memory link and returns the first frame whose state differed, or -1. It is test-only code and never ships to players.

Our room server (in-house)

We run it for our own games; it is not a service you can sign up for. One Durable Object per numbered room seats players in arrival order. Once the room is full it sends each one a WELCOME with their seat, the player count and one shared seed, then relays every message, stamped with the sender's real seat. WebSockets use the hibernation API, so an idle room holds no running object, and rooms store nothing.

Every cap is in code with a ceiling config can't lift: 16 rooms (ceiling 256), 2 players a room (ceiling 8), 90 messages a second a socket with bursts to 180, any message that is not 12 bytes closed at once, 120 s idle and 30 minutes per room. Lockstep at 60 Hz sends about 62 messages a second: at 12 bytes each, under 1 KB a second of payload per player.

What is not there yet

  • Accounts. The gate is a demo gate: invite codes, stored only as SHA-256 hashes.
  • Leaving. A seat that leaves mid-game makes the others wait.
  • Rollback and authority. The confirmed input log is there for a rollback layer, and some games will need an authoritative room. Neither is built.

Questions

Does the server run the game?

No. It seats, seeds and relays. The game runs in each browser, the same on every one.

Can I use your room server?

No. It is in-house: it runs our games and isn't a hosted service. The Build your own room server guide has the wire protocol presence speaks, the one-room-one-object pattern and a threat checklist, so you can run one like it.

Which game uses it first?

Battlefield Blitz, on DOM VIRUS: its step can be driven by presence, with a fixed arena so every screen runs the same match.

All notes