Helix Empire — 10,000 players in one real-time world
A browser-based, real-time space strategy whose core is genetics: players breed castes of creatures by editing DNA. The hard part is the engine — 10,000 players in a single shared world on one server, updating live. I designed the architecture around the real bottleneck (outbound traffic, not CPU) and proved it with a measured load test: 237.6 Mbit/s at 10k players, 24% of the budget. Built solo, currently in alpha.
- Role
- Solo architect-engineer
- Stack
- Rust · WebAssembly · WebTransport / QUIC · Event sourcing · Area of Interest · Hetzner bare-metal
- Period
- 2026 — present
The problem
Helix Empire is a real-time, browser-based space strategy game. Its core isn't building — it's genetics: a player breeds castes of creatures by editing their DNA (hence "Helix", the double spiral). Genes set traits, traits decide how creatures perform in different environments, that drives the economy and the army, which drives science, which unlocks new edits to the genome. A closed loop: genes → traits → economy → science → lab → genes again. Empire strength = genes × planets × technology.
Technically, that means thousands of players in one shared world, alive in real time — the economy ticks, creatures breed, fleets move, resources change every second — and all of it has to reach every player's browser without lag.
Most online games hide their scale: they split players into small rooms of 20–50, or into shards. I set a harder target on purpose — 10,000 concurrent players in a single world on a single server — because that constraint is exactly where the interesting architecture lives.
The four walls
Say "10,000 in one world" out loud and four walls appear that a naive design smashes into.
- Broadcasting everyone-to-everyone is O(N²). If every player must know about every other,
one tick is
10,000 × 10,000 = 100,000,000pairs — several times a second. Physically impossible. Double the players, quadruple the work. - The dominant cost is traffic, not CPU. The unintuitive hosting fact: outbound traffic (egress) is the expensive part — tens of times pricier on hyperscalers than on bare metal. If every tick ships many bytes to every player, the bandwidth bill kills the project long before CPU does. So the architecture has to be designed around minimizing traffic.
- One world wants one server. Spreading a world across machines forces distributed consensus (which server holds the true state?) — slow and complex. I chose single-writer: exactly one process may mutate a world. No races, no consensus. (It has a nasty trap — more below.)
- A thin client can't keep up. If the server computes everything and the browser only draws, the server is the bottleneck. The client has to be thick — able to reconstruct the picture from a minimum of data.
The stack — chosen against the walls, not the trends
The point of architecture is that technology serves constraints, not fashion. Every choice here answers a specific wall.
Rust + a deterministic core compiled to WebAssembly
The simulation core is written once in Rust and compiled both to a native server binary and to WASM for the browser. The same code runs on both sides, so the thick client can re-compute the world from compact "seeds" and predict ahead — moving load off the server (walls #2, #4). Rust's predictable memory and no garbage collector mean more players per server.
Why: one source of truth for the simulation means client and server physically cannot disagree — convergence is structural, not a test I hope passes.
WebTransport / QUIC, single-writer worlds, event sourcing
Realtime frames travel over WebTransport / QUIC (a fast binary stream over UDP, without TCP's head-of-line stalls), with a WebSocket fallback. Each world has one writer, and its history is an event stream — so there's no in-world consensus, and state can be rebuilt or audited from events (wall #3).
Why: single-writer buys consistency for free; event sourcing buys recoverability. The cost is a read-contention trap, which the read-model caches below pay off.
What I built, step by step — measuring at every step
I didn't build it all at once. I went in layers, measuring at each one — because you can't optimize what you haven't measured.
Step 0 — baseline. Before fixing anything, I measured how bad it was: state reads spiking to 4–10 seconds, push to the client at roughly one frame per 20 seconds. That gave me a number to beat.
Steps 1–3 — kill single-writer contention. Here's the trap. If one process mutates the world, then while it applies a tick (writing to storage — slow), every reader waits on the same lock. Open the defense screen and you hang for 10 seconds because the server is mid-save. The fix: lock-free read-model caches in RAM, updated incrementally and read without the write lock.
The architectural move: separate reads from writes. One writer still owns consistency; readers read RAM projections and stop waiting. Contention vanished, every endpoint went stably sub-second.
Steps 4–5 — send the difference, not the whole state. Even with fast reads, each tick still
shipped the full state — re-sending mountains of things that hadn't changed. The fix: deltas.
A mathematically checked WireSessionStateDelta where between(prev, next) builds the difference
and apply(prev, delta) replays it. A convergence test proves a chain of deltas folded by the
client exactly reproduces the server snapshot — and since client and server share the same Rust
code, they can't drift. Lost a frame? The client notices (ResyncRequired) and asks for a full
snapshot.
Step 6 — Area of Interest cuts O(N²) to linear. A player doesn't need all 10,000 — only their
spatial and diplomatic neighbors. AreaOfInterest + filter_frame clip each delta to what a
subscriber actually cares about, while colony-wide context (chat, trades) is preserved. Quadratic
broadcast becomes linear: traffic grows as N × (size of interest area), not N².
Step 7 — the honest last mile. This is the most important part of the story. A load test revealed the pretty "320 Mbit/s" number rested on a hardcoded assumption of 32 bytes per player delta. I plugged in the real binary codec and measured: a real delta was 104 bytes, because I shipped a player's entire profile (11 resource fields) even when one field changed. 104 vs 32 is 3.25× over — a projected ~1.04 Gbit/s at 10k. The honest answer at that moment was: "no, on real traffic I don't hold 10k."
The fix was a per-field delta, WirePlayerDelta:
player_id (4 bytes) + changed-field bitmask (2 bytes) + ONLY the changed values
If population, food and science changed, the wire carries exactly those three plus the mask —
4 + 2 + 4 + 8 + 8 = 26 bytes instead of 104. The 16-bit mask says which fields follow; an
unchanged field costs nothing.
How it's proven — and why the number is trustworthy
Architecture without proof is a promise. I proved it with numbers: round-trip and convergence unit tests, contract tests on every port, and 42 e2e tests in real Chromium over WebTransport (including "receives server ticks over a realtime socket with no polling" and "two players in one world sync in real time").
The headline proof is a 10,000-bot swarm test that measures egress with the real binary codec — no hardcoded constants. It builds 10,000 bot clients in one world, runs a real authoritative tick, applies each bot's visible deltas through real AoI filtering, and sums the measured bytes.
The figure is defended by a check:load gate: a report is verified on every run, and the
projection is derived from the measured value, not an assumption. If someone accidentally
bloats the delta format, the gate fails.
The honest boundary
Architectural maturity isn't only getting a good number — it's being honest about its limits.
Proven: by compute and latency, fan-out to 10,000 bots stays within 200 ms with large headroom; by egress, 237.6 Mbit/s (24% of a 1 Gbit/s budget), measured with the real codec, not assumed.
Not yet proven: this is a projection from one measured tick at 10k (bots in one process), not a live cluster of 10,000 real QUIC sockets under load for hours. Sustained CPU/RAM behavior under a multi-minute stream, and the per-subscriber AoI channel for the thick WASM client, are the next layer of work. But the biggest risk — egress, which I flagged myself as over budget on the real format — is closed and measured.
What's the architect here, not just the developer
- Named the real bottleneck: egress, not CPU. The whole stack serves that one conclusion. Get it wrong and you optimize the wrong thing.
- Separated reads from writes: single-writer for consistency + lock-free RAM projections for reads. Killed contention without sacrificing correctness.
- Reduced O(N²) to linear via Area of Interest — without it, 10k is impossible in principle.
- Deltas over snapshots, with one codebase on server and client, so convergence is guaranteed by architecture, not by luck.
- Didn't trust the pretty number until I measured it. Found "32 bytes" was an assumption, measured the real 104, designed the per-field delta, drove it to 26, and proved it by measurement.
- Locked the result behind a gate so a regression can't slip by unnoticed.
An engine that measurably serves 10,000 players in one world at 24% of its traffic budget isn't luck or a single trick. It's a sequence of architectural decisions, each answering a specific wall, each backed by a number.
Stack snapshot
| Layer | Choice | Wall it answers |
|---|---|---|
| Simulation core | Rust, deterministic, compiled to native + WASM | thick client, more players/server (#2, #4) |
| Transport | WebTransport / QUIC, WebSocket fallback | low-latency binary stream (#2) |
| World model | Single-writer + event sourcing | no consensus, recoverable (#3) |
| Reads | Lock-free RAM read-model projections | contention-free sub-second reads |
| Updates | Per-field binary deltas + Area of Interest | O(N²) → linear, minimal egress (#1, #2) |
| Hosting | Hetzner bare-metal | egress 20–40× cheaper than hyperscalers (#2) |
Helix Empire is in alpha, built solo — product design, Rust simulation core, WASM client, transport, the read/write split, the delta + AoI pipeline, the load harness, and the measured proof. It will soon be playable at helixempire.com.