fedorthinks
All work
In progress2026 — present

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.

  1. Broadcasting everyone-to-everyone is O(N²). If every player must know about every other, one tick is 10,000 × 10,000 = 100,000,000 pairs — several times a second. Physically impossible. Double the players, quadruple the work.
  2. 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.
  3. 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.)
  4. 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.

0.002s
defense read, was 0.002–4.4s with spikes
~0.006s
events feed, was ~0.2s
read = write
separated: readers no longer wait on the writer

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.

26 bytes
typical player delta, down from 104
237.6 Mbit/s
egress at 10,000 × 5 ticks/s
24%
of the 1 Gbit/s budget — ~76% headroom
≤ 200 ms
tick fan-out latency at 10k

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

  1. Named the real bottleneck: egress, not CPU. The whole stack serves that one conclusion. Get it wrong and you optimize the wrong thing.
  2. Separated reads from writes: single-writer for consistency + lock-free RAM projections for reads. Killed contention without sacrificing correctness.
  3. Reduced O(N²) to linear via Area of Interest — without it, 10k is impossible in principle.
  4. Deltas over snapshots, with one codebase on server and client, so convergence is guaranteed by architecture, not by luck.
  5. 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.
  6. 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

LayerChoiceWall it answers
Simulation coreRust, deterministic, compiled to native + WASMthick client, more players/server (#2, #4)
TransportWebTransport / QUIC, WebSocket fallbacklow-latency binary stream (#2)
World modelSingle-writer + event sourcingno consensus, recoverable (#3)
ReadsLock-free RAM read-model projectionscontention-free sub-second reads
UpdatesPer-field binary deltas + Area of InterestO(N²) → linear, minimal egress (#1, #2)
HostingHetzner bare-metalegress 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.