Skip to content

Performance

Terminal window
node bench/run.ts # about a minute
node bench/run.ts --sockets # also 2,000 real sockets (--sockets=200 for a small run)

run.ts prints one table with the measured value, the target and pass or FAIL; a FAIL is a result, not an error, and the exit code is 1 only when a script crashes. Not part of the test gate. Results as JSON go to the temp folder.

Script Measures Target
hubbench.mjs data down per player (16 players, 32 moving monsters) 11 KB/s or less
fxbench.mjs frames per player a second with one-off effects 25 or less
interp-real.mjs others’ movement through the real room.js (interp.mjs: the older synthetic model) under 1% jump frames, speed within 10%
savetest.mjs world writes after a failed load; fuzzed saves (--store <module> also times a host app’s own store) 0 writes, 0 escaped errors
clientcopy.mjs the page’s cost per frame at 60 KB of shared state under 0.1 ms
wsclient.mjs + wsserver.mjs (--sockets) event loop delay of a real server, 2,000 local sockets; --url ws://... drives a deployed demo server p99 under 10 ms

Live load test (a host app on a 6-core, 12 GB server, the load client on the same machine, 16 players and 32 moving monsters per room):

Players Joined Dropped Data per player Total out Server event loop p99 Delta age p99
208 (13 rooms) 208 0 12.2 KB/s 21 Mbit/s 12 ms 1 ms
1,008 (63 rooms) 960 (48 refused: over the 50 rooms per namespace cap) 0 5.5 KB/s (half rate: busy mode on) peak 88 Mbit/s 32 ms (max 106) 3 ms

At about 1,000 players the loop passed the 30 ms line, so the server switched to busy mode (rooms over 4 players at half rate) and 3 rooms over their own 250 KB/s budget were slowed; everyone stayed connected. Room for more needs a second process or a bigger server; the data cap (maxOutMbit 100) is the next limit.

hub.stats().live: frames, bytes and messages per second, tick time p50/p99/max (the last 1,200 sub-ticks of 10 ms, about 12 s), slow readers, matches in progress, saves ok/failed, failed loads, world writes a minute, the 5 busiest rooms (players, bytes out a second, shared size; no names or ids) loop (event-loop delay, set by the host process: hub.loop = {p50, p99, max}), pressure (the server is over its data cap or its loop runs late) and slowedRooms (rooms over their own budget). hub.stats() also has refused: turned-away and dropped counts by reason (full, joins, address, accept_rate, busy_rooms, silent, room_error, room_budget, pressure, too_fast, …). A host app can show the whole stats() on an admin page and a few totals in its health check.