Alex66

Alex66

Anyone pushed the BEAM for real-time simulation workloads? I built a multiplayer game server framework pushing 10,000 Entities @ 30Hz

I’ve been building an open-source multiplayer game server framework in Elixir, and I wanted to share the results.

The Challenge: Can the BEAM handle MMO-scale real-time simulation? The conventional wisdom says “use C++ for game servers.” I wanted to prove otherwise.

The Results:

Metric Target Achieved
Tick time (10K entities) < 29ms 8ms avg, 13ms max
P99 tick time < 29ms 11.1ms
Bytes per entity < 20 18 (template bitpacking)
Per-player bandwidth < 1 MB/s 264 KB/s
Full broadcast bandwidth < 100 MB/s 5.2 MB/s
Crash recovery < 100ms 4.6ms

Key Techniques:

  • Bucket-parallel tick loop — Chunk entities across schedulers, not Task-per-entity

  • Fused encoding — Tick + serialize in one pass while data is hot in L1 cache

  • :maps.from_list — 9x faster than Map.put in a reduce (TIL!)

  • IO lists everywhere — Never concatenate binaries, let writev gather fragments

  • Time-travel debugging — Circular buffer with structural sharing for replay

  • Cross-zone shadows — PubSub-based visibility across zone boundaries

  • Async persistence — Snapshot + journal with atomic writes

The “aha moment”: Killing the 68ms sequential serialization bottleneck. Fused parallel encoding brought it down to 2ms for a typical player’s visible area (500 entities).

Architecture highlights:

  • Hybrid entity model (players as processes, NPCs as data in zone state)

  • ETS spatial grid for O(1) neighbor queries

  • Binary protocol with reliable_seq gap detection

  • Dead reckoning for shadow entity interpolation

  • Hysteresis on zone boundaries to prevent oscillation

Test suite: 340 tests, 0 failures

Still early days, but the engine handles the “10K goblin stress test” without breaking a sweat. Next up: TypeScript client SDK and a simple LiveView visualizer.

Curious if others have pushed the BEAM for real-time simulation workloads. What’s your experience?

Most Liked

Alex66

Alex66

Hello @nxy7

Yes, you are right the testing ground is not well explained, let me complete the informations below:

The test scenario:

  • 10,000 NPC entities (not players) in a single zone

  • Each entity runs behaviors every tick (wander, chase, spatial awareness)

  • Each tick: query neighbors, update position, serialize state

  • 30 ticks per second

Player model:

  • Tested with 100 simulated players

  • Each player has AOI (Area of Interest) ~500 visible entities

  • Per-player delta snapshot: ~2ms

  • Full 10K broadcast (worst case): 22ms

What entities DO per tick:

1. Spatial query: "Who's near me?" (ETS grid lookup)
2. Behavior: Decide action (chase/flee/wander)
3. Update: New position/velocity
4. Encode: Serialize to binary (18 bytes)

Scaling to 10K × 10K:

Split zones. Each zone handles 10K. Shadows (PubSub) connect boundaries. Topology scales horizontally—add nodes, add zones.

Zig/Rust sidecar:

Architecture supports it via Ports. Heavy math (pathfinding, UMAP, physics) can offload. BEAM handles state + networking, sidecar handles computation.

Best,

pikdum

pikdum

Nice! I’ve been (slowly) working on a World of Warcraft server, Thistle Tea, that has some similarities.

In Thistle Tea, players and NPCs are processes, but I’ve abstracted the interface to use GUIDs. The idea is that the boundary process layer will be straightforward enough to swap for something that groups entities by zone instead if it ever becomes necessary, without needing to change (much) game logic code.

IO lists are a good idea, they’ve been on my list to benchmark sometime but haven’t got around to it. Right now building packets is just binary concatenation. Bandwidth measurements are a smart idea, I have latency metrics but bandwidth would helpful to understand the entire system performance.

I’m also using ETS for a spatial grid and it’s been working really well. Recently also started using ETS for per entity metadata that other processes need access to in hot loops, that seems to work decent to avoid the message passing overhead where necessary.

I’ve mostly been focusing on building out functionality, recently got a rudimentary NPC AI wired up using behavior trees to get the basics of combat working. Stuff’s tricky because the client really likes crashing if packets are even slightly malformed, and some of the expected packet layouts aren’t very clear, lol.

It uses a Rust NIF for pathfinding, which would’ve been a pain to implement from scratch. Still a bit of wonkiness there for me to debug sometime, though. Mobs like to stutter around a bit when repathing, which makes me think the server isn’t perfectly simulating movement the same way the client is (or some other bug).

Here are a few blog posts I wrote about the process if you’re interested.

Alex66

Alex66

2° Round - lessons from the metal: optimizing 10,000 entities on a DL380 (Xeon vs i7)

The Setup
After my last post about 10k entities on a modern i7, I moved the project to a refurbished HP DL380 Gen9 2014 (Dual Xeon E5-2650 v3 @ 2.30GHz). The goal was to see how the BEAM handles high-frequency simulation on older, high-core-count server hardware using Docker.

The “Xeon Wall”
The first run in Docker was a failure. Code that ran in 11ms on my 5GHz desktop workstation hit 240ms+ on the server. The lower single-core clock speed (2.3GHz) exposed serial bottlenecks that were previously hidden:

  1. The QPI/NUMA Gap: Docker was straddling both physical CPU sockets. Moving data between Socket 0 and Socket 1 (Distance 21) added massive jitter to the memory-heavy AI passes.

  2. The Socket Tax: Moving 320KB of entity data through Linux kernel UDS sockets every 33ms to our Rust physics sidecar was eating ~90ms in context-switching and marshalling.

The Fixes
We had to move from standard “Web” patterns to “Systems” patterns to get under the 33ms budget:

  • Shared Memory Slab: We replaced Unix Domain Sockets with a shared memory segment in /dev/shm. Elixir and Rust now communicate via a 64-byte aligned “Foundry Slab.” Marshalling time dropped from 88ms to < 1ms.

  • Docker NUMA Pinning: We used --cpuset-cpus and --cpuset-mems to lock the container to Node 0 (Cores 0-9 and their hyperthreads). This kept the BEAM schedulers and the memory local to the same physical silicon.

  • The N-1 Pipeline: We deferred the physics merge to the start of the next tick. The workers now “gulp” the previous physics result while they are already reading from ETS for the AI pass, reducing ETS write-pressure.

The Results

  • DL380 (Xeon E5-2650 v3): 22-24ms P99 (Solid 30Hz in Docker )

  • i7-13700 Workstation: 7-10ms P99 (Running the same “Foundry Slab” architecture)

Takeaway
Running high-performance Elixir in Docker is totally viable, but hardware is not a transparent layer. On older server metal, the “Postman” (I/O) is often the bottleneck, not the “Engineer” (The Logic). By moving to Zero-Copy SHM and respecting NUMA topology, we reclaimed over 200ms of tick time.

Where Next?

Popular in Discussions Top

Qqwy
I would like to spark a discussion about the static access operator: .. For whom does not know: it is used in Elixir to access fields of...
New
pillaiindu
In django there is a cache framework backed by memcached. Rails also puts a lot of emphasis on caching, and even the idea of russian-doll...
New
acrolink
How does the two languages compare when it comes to server side application development? Any experiences or ideas? Thank you.
New
boundedvariable
I am going through the kafka architecture. All the features what the kafka is providing are already in Erlang. I would like hear your opi...
New
pdgonzalez872
If this has been asked here before, please point me to where it was asked as I didn’t find it when I searched the forum. Maybe a mailing ...
New
chulkilee
Here are the list of HTTP client libraries/wrappers, and some thoughts on HTTP client in general. I’d like to hear from others how they w...
New
chuck
Let me start by stating an assumption: Phoenix is a great approach to building REST APIs. There are many reasons for this, but I will ass...
New
lucaong
Hello Elixir and Nerves community, I have been working for a while on an open-source embedded key-value database for Elixir, that I call...
230 14027 124
New
Ankhers
Just a little information upfront. Generally speaking, if I feel like I need to either break a pipe chain or use an anonymous function in...
New
100phlecs
Are there any downsides, like perf issues, to putting all functional components in CoreComponents as long as you prefix it with the conte...
New

Other popular topics Top

JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

Latest on Elixir Forum

We're in Beta

About us Mission Statement