# 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.

- **Author:** Fedor Chishchin
- **Role:** Solo architect-engineer
- **Period:** 2026 — present
- **Stack:** Rust · WebAssembly · WebTransport / QUIC · Event sourcing · Area of Interest · Hetzner bare-metal
- **Source:** https://fedorthinks.com/work/helix-empire

---

## 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.

## 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

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

| 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**.
