Combat soak: 50 players, 500 creatures, one zone (2026-10-11)

The raw numbers are in soak-2026-10-11.json. The test is apps/bezgelor_world/test/combat/soak_test.exs. All the data is synthetic: in-process headless players, no real users.

Scenario

Parameter Value
Duration 300 s
Players 50 (all six classes, level 15)
Creatures 500 hostile, levels 11-15, interrupt armor 0 (60%), 1 or 2 (20% each)
Area 300 × 300 m, one zone instance
Player behaviour Every 1.2-1.8 s: top up resources, cast a random level 1-15 kit ability at a creature within 25 m (else a random one); move 5 m in a random direction 25% of the time; wake up when dead
Creature behaviour Server AI: aggro, chase, swing, leash and evade, die and respawn
Viewers Each player views the creatures within 120 m, so movement and combat packets are encoded and sent
Zone tick 250 ms (creature AI, auras, regeneration, scripts, loot)
Client oracle Off, to keep the shared host's load down. The zone scenario tests and the e2e tests oracle-check every packet.

Results (run of 2026-10-11)

Metric Value
Zone ticks 1,194 (about 4 per second, on schedule)
Tick p50 / p95 / p99 / max 6.4 / 19.9 / 22.7 / 106.9 ms
Tick budget 250 ms; the asserted p95 limit is 100 ms
Zone mailbox, max 59 messages
Zone process heap 7.3 MB at the first 10 s sample, 9.1 MB at the last (it moves between 4.5 and 9.3 MB with GC)
VM processes 237 before, 289 at peak, 287 after the players left and the zone stopped (the 50 fake connections live until the test ends)
Player casts 7,536 (25.1 per second)
Player wake-ups 1,480 (the creatures fight back)
Creatures in combat at the end 112
Consistency checks 30 (one every 10 s) plus a final check; all passed
Crashes none

Each consistency check verifies that:

  • no unit has negative health, or more than its maximum;
  • dead creatures hold no auras, threat or target;
  • creatures in combat have a threat list of known units;
  • idle creatures have no threat;
  • no player is flagged in combat while on no threat list;
  • no expired aura lingers;
  • dead players have a ghost and no pending cast.

Earlier runs the same day

Run Ticks p50 p95 p99 max Mailbox max Heap first → last
60 s, unconstrained, machine busy 240 8.6 ms 27.4 ms 37.6 ms 95 ms 61 7.3 → 9.1 MB
300 s, unconstrained, machine at load 50-100 1,187 7.4 ms 21.9 ms 35.0 ms 116 ms 64 7.3 → 7.4 MB
300 s, under scripts/limited.sh 1,190 6.9 ms 19.8 ms 22.6 ms 109 ms 66 7.3 → 9.1 MB

The first 300-second run found a crash, since fixed. A dead player whose ghost couldn't be built (its database lookup failed) took the zone process down on leaving.

Machine

  • 20 cores and 31 GB RAM: a shared developer host, with other sessions' test suites running at the same time.
  • The run was limited by scripts/limited.sh:
    • a systemd user scope with MemoryMax 6 GB, swap capped at 1 GB and CPUQuota 400%;
    • nice 10, ionice idle;
    • Erlang +S 4:4 +SDcpu 2:2 +SDio 2;
    • the machine-wide heavy-job lock.
  • Software: Erlang/OTP 28, Elixir 1.19.6, PostgreSQL 16 (the zone writes durability on death through the Ecto sandbox).

Reproduce

From a checkout with dependencies fetched and PostgreSQL running (see the repository's testing notes):

cd apps/bezgelor_world
SOAK_SECONDS=300 \
SOAK_REPORT=$PWD/../../docs/community/test-reports/soak-$(date +%F).json \
LIMIT_HEAVY=1 LIMIT_TIMEOUT=30m \
  ../../scripts/limited.sh mix test test/combat/soak_test.exs --include soak

SOAK_TICK_P95_MS changes the asserted p95 limit (default 100). Without scripts/limited.sh, run plain mix test ...; the numbers will reflect your machine.

What it does not cover

  • Idle AI across a whole continent. Issue #93: Alizar's tick p95 is about 400 ms with one player, because every creature on the continent runs idle AI. This soak uses one 300 m area.
  • Network I/O. The connections are in-process fake sockets: packets are encoded but not sent over TCP.
  • Database writes, beyond durability on death.

See also the combat audit summary.