Docs / Performance

Performance & benchmarks

What Asobi handles, how to measure it, and how to tune tick rates, zone sizing, and BEAM knobs when you need more headroom.

Reference benchmarks

Measured on a single 4-core VM (2026-04 runs, asobi_bench):

 Scenario                              Players   Rate    CPU     RAM
 ─────────────────────────────────────────────────────────────────────
 Match: 10-player arena, 20 Hz ticks   10        20 Hz   ~3%     ~40 MB
 Match: 100 concurrent arenas          1000      20 Hz   ~35%    ~800 MB
 World: 500 players on 128K x 128K     500       20 Hz   ~55%    ~208 MB
 WebSocket fan-out: chat broadcast     10000     10 Hz   ~25%    ~600 MB
 Matchmaker tick (10K tickets)         10000     1 Hz    ~10%    negligible

Worst-case p99 tick latency stays under 5 ms in all scenarios. Full methodology + reproducer: asobi/bench.

Tick budget

Everything in a tick must finish before the next tick fires. Default 20 Hz = 50 ms budget. If zone_tick/2 takes 60 ms, you miss the next tick; state updates bunch up and become bursty. Keep tick work to <50% of the budget to leave headroom for input spikes.

Tuning ticks

  • Lower the tick rate (10–15 Hz) if you don't need 20 Hz visual smoothness.
  • Split heavy per-entity work across frames (round-robin): see the cookbook AI-stepping recipe.
  • Move expensive deterministic sims to NIFs if you've profiled them as the bottleneck (rare).

Zone sizing

  • Too small: many zones, more subscription churn on movement, higher delta overhead.
  • Too large: one zone dominates a CPU core; fewer parallelism opportunities.
  • Rule of thumb: target 10–50 entities per zone under peak load, grid cell = 2× max interest radius.

Delta compression

Only changed fields ship. If your state map is deep (nested entities, lists of lists), diffs can be expensive to compute. Flatten hot-path state into top-level fields for cheap diffs; keep per-player “what did they see last tick” in match state so get_state short-circuits when nothing changed.

BEAM knobs

# vm.args — baseline for a 16-core host
+sbt db                       # bind schedulers to cores
+S 16:16                      # schedulers = online cores
+A 64                         # async IO threads for file/socket reads
+P 10000000                   # max processes (matches + zones + sessions)
+K true                       # kernel poll (Linux)
+zdbbl 32768                  # distribution buffer, 32 MiB (cluster only)

Profiling

  • recon for live-node inspection (top processes by memory/reductions).
  • fprof / eprof for deterministic CPU profiles of specific callbacks.
  • msacc for scheduler utilization / where time goes.
  • observer_cli for a continuous dashboard.

Lua-specific

  • Luerl is interpreted. Lua callbacks run 3–10× slower than equivalent Erlang. For a 10 Hz arena this is fine; for 60 Hz physics, write the hot loop in Erlang and call from Lua.
  • Avoid allocating big tables every tick — reuse state. Luerl's GC is per-VM and stops the tick.
  • Set instruction_limit in asobi_lua config to prevent pathological scripts from hogging a tick.

Load testing

The asobi_bench repo includes tsung-style scripts for WebSocket + REST load. Run against a staging node before major releases.

Where next?