June 24, 2026 · KinematicSoup Team

How We Synced 5,000 Enemies in a Multiplayer Survivors Game

We build games on Reactor as a way to dogfood our own engine, and the latest one is a multiplayer survivors-like. The genre had a recent hit in Megabonk, and the appeal is familiar: you stand in a rising tide of enemies and cut them down in swaths. Our twist is that it is multiplayer, and there are far more enemies. A typical match has 5,000 to 6,000 of them on the field at once, and clearing a crowd that size has a particular cathartic pull to it.

The gameplay loop was never the hard part. The question we cared about was whether we could keep 5,000-plus server-authoritative enemies synced to every player without the bandwidth or the frame rate falling apart. Here is how it held up.

A typical match: 5,000 to 6,000 server-simulated enemies, synced to every player.

A week of greybox

The first playable version took about a week, built with Reactor and our in-development DOTS extension. None of that week went into networking. There was no art either: the world was a flat plane ringed by a wall, players and enemies were capsules, and projectiles were spheres, with no sound or music. It was as greybox as greybox gets. The time went into the game concept and the enemy behavior, because the sync layer was already handled. That is the whole point of dogfooding the engine: we get to spend the week on the game instead of on serialization and state replication.

The game targets 2 to 4 players in co-op, a choice driven by the design rather than any limit in the engine.

Boids on the server

Every enemy is server-authoritative. The flocking is a boids algorithm running on the Reactor server, so the simulation is consistent for all players and there is nothing for a client to fake. Each enemy is an entity with an Update function that only operates on itself, which lets us use Reactor’s parallel updates: the engine runs those Update calls across multiple threads. For this game we used two threads. We did not reach for Reactor’s virtual-player system, where each entity drives itself through the server-authoritative controller; the self-contained boids approach was simpler for what we needed.

To keep the CPU in check, the boids run on a schedule rather than every tick. An enemy far from any player updates once every 8 frames, about 3.75 times per second, and an enemy near a player updates 15 times per second. This is simulation level of detail, applied to compute. It is worth being clear that it is separate from network level of detail, which we will come back to, because we are not using any.

With all of this in place, the server holds at roughly one hardware thread most of the time. A few subsystems push it past a single vcore in bursts, and those are an optimization target rather than a wall. Simulating 5,000 to 6,000 flocking enemies on about one core is the kind of headroom that makes a crowd this size practical to host.

The ceiling sits well above what the game needs. Early in that first week, before we settled on the 5,000 to 6,000 range, the greybox was syncing 10,000 entities, around 11,000 networked objects in total, to every client at once, on four server cores. We brought the count down because the game plays better there, not because the engine ran out of room. Between the smaller count and the scheduled updates, that four-core load came down to roughly one.

The network numbers

The server runs at a 30 Hz tick rate and sends a network frame every second tick, for a 15 Hz send rate. At those settings, with 5,000 to 6,000 entities in the play area and no network level of detail, server bandwidth sits between 50 and 70 KB/s per player.

The compression behind that number:

  • A transform delta compresses to 7 bits.
  • A full object sync, sent when an object spawns, compresses to about 4 bytes.

None of that was hand-tuned. We set the fidelity we wanted and the rest is innate to the netcode. Two details show how automatic it is. Enemies spawn at random positions and carry a full 3D transform, but the game never uses their rotation. Reactor notices that the rotation never changes and compresses it down to near zero bits on its own. Bullets are a second case: they spawn from the player’s avatar, so their positions cluster near a known point, and Reactor takes advantage of that proximity to compress them, again with no instruction from us.

Spawning and destroying is a larger share of the traffic than people usually expect. In later levels players fire hundreds of bullets per second, each of which spawns an object, and enemies are destroyed continuously as their health reaches zero. Replacement enemies spawn once per second, batched into a single update, while bullets and deaths happen on any frame. Together, this object churn accounts for about 20 percent of the total bandwidth. High spawn and destroy rates are where a lot of engines get expensive, so it is a part of the budget worth watching, and at 4 bytes per spawn it stayed cheap here.

Why no network LOD

The obvious next optimization is network level of detail: send updates for distant objects less often. We are not doing it, for a simple reason. The compression already performs well enough that we do not need it. The 50 to 70 KB/s figure is what the game costs before any culling at all.

Network LOD would still help. It would cut distant-object updates by around 30 percent and reduce encoding time. The catch is that it adds a cost at the boundary: when an object crosses from far to near, it needs extra correction data, which is an inherent side effect of using delta coding for position. When your overall compression ratio is already strong, that tradeoff is not always worth making. We have it filed as a future option, but for now the effort is going into game design and gameplay, not squeezing a bandwidth line that is already comfortable.

Choosing the rates

The 30 Hz simulation rate and 15 Hz network rate were deliberate, one for server performance and one for bandwidth.

Dropping the simulation from 60 Hz to 30 Hz halves how often the server runs its per-tick work: input handling, world-state checks, network status, and the rest. It also gives the simulation more room. At 30 Hz, each tick has roughly a 30 ms window to finish, and while it rarely needs all of it, the budget is there when a heavy frame comes along.

The 15 Hz send rate suits the game. Players auto-attack, so their moment-to-moment input is light, and the two active abilities, a special attack and a dash, stay responsive at this rate. A twitch shooter would want more; a survivors game does not.

One last detail we like: if encoding or simulation runs long in real time, Reactor automatically reduces the network sync rate to hold a steady cadence and keep server simulation time aligned with the wall clock. The send rate is a target, not a guarantee, and the engine protects the cadence on its own.

The client side

Rendering 5,000 enemies is its own problem, separate from networking them. On the client we use DOTS to handle the entity count, and the result is a steady 60 frames per second on moderate hardware, GPU-limited rather than CPU-limited. The simulation is on the server and the rendering scales on the client, and the two meet in the middle at a crowd that would be impractical to push through a conventional setup.

What it adds up to

A multiplayer survivors-like with 5,000 to 6,000 server-authoritative enemies, one server core, 50 to 70 KB/s per player, a steady 60 FPS client, and no network LOD, built to a playable greybox in a week. The part we want to underline is that the efficiency was not the work. We set a fidelity target and the netcode did the rest, from the 7-bit transform deltas to the dropped rotation to the proximity-compressed bullets. That is what lets a small team spend its week on the game instead of the plumbing.

That automatic efficiency is the core of what Reactor does. If you are building something that needs to put a lot on the wire, see what Reactor can do.