Load testing your game
cks-loadtest is an
open-source load tester for games built on the Replication API. It simulates
any number of native game clients that sign in, enter your app, and walk
around the world exchanging actor updates — using exactly the flow documented
in this tab: passwordless-compatible sign-in, mint an app token,
serverWithLeastClients, then HMAC-signed ACTOR_UPDATE_REQUEST_2 traffic
over UDP. Because it drives the same public APIs and wire protocol as your
real clients, its numbers reflect what real players will experience.
It is a lightweight C++ program: a single host drives thousands of simulated clients. Run it natively on Ubuntu or anywhere Docker runs.
How it works
Management API Game API UDP
────────────── ──────── ─────────────
login / register ──▶ (per derived account)
mintAppToken ──▶ app token + gameApiUrl
serverWithLeastClients ──▶ session registered
◀── ip4, clientPort
(wait ~1.5 s)
ACTOR_UPDATE_REQUEST_2 ────────────────────────────▶ at LT_UPDATE_HZ
◀── actor notifications, bundles
(tokens auto-refresh before expiresAt; reconnects handled automatically)
You supply one email and password. The tool derives a deterministic
account per simulated client with plus-addressing — alice@studio.com
becomes alice+lt-0000@studio.com, alice+lt-0001@studio.com, … (pattern
configurable). Accounts are registered on the first run and simply logged in
on later runs, so repeated tests are idempotent. Each simulated client mints
its own app-scoped token, is assigned a server, and sends updates at the
rate you configure while walking a random path near the world origin.
Set up your game for load testing
The load tester is tier-agnostic: it never inspects or manages access tiers, entitlements, or limits. It mints tokens and sends traffic; making the simulated accounts entitled to your app is the studio's job, the same as for real players (see Set up a game & entitle players).
- Open-by-default apps need no setup. If your app kept its free default
tier, every derived account is auto-entitled on its first
mintAppToken. - Restricted or paid apps must pre-grant access.
mintAppTokenreturnsFORBIDDENfor an unentitled account and the run aborts with the account's email. The derived emails are predictable, so grant them ahead of time — the repo shipsscripts/grant-access.sh, an owner-side helper that grants every derived account a chosen tier viagrantAppAccess(run the load tester once first so the accounts exist). - The granted tier must carry runtime permissions — at minimum
access. A permission-less tier mints tokens successfully, but every spatial send is rejected withUNAUTHORIZED(code 7). See Operations → Permissions.
Simulated clients generate real replication traffic, metered like any other client traffic. On the shared environment this draws down your wallet and counts toward spend caps — an aggressive test can runtime-deny your own app (see Operations → App suspended or over budget). Prefer a dev or dedicated environment for sustained load tests, and set spend caps deliberately before testing on shared infrastructure.
Registration also sends one confirmation email per new account; with the default plus-addressing pattern they all land in the base inbox on the first run. Confirmation is not required for the test to run.
Install and run
Native (Ubuntu 24.04):
git clone https://github.com/CrowdedKingdoms/cks-loadtest.git && cd cks-loadtest
sudo apt-get install build-essential cmake libcurl4-openssl-dev libssl-dev
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j"$(nproc)"
Or with Docker:
docker build -t cks-loadtest .
Configure with CLI flags, LT_* environment variables, or a KEY=VALUE
file (precedence: CLI > env > file). A minimal run:
./build/cks-loadtest \
--email you@studio.com \
--password 'your-password' \
--management-api-url https://api.crowdedkingdoms.com \
--app-id 42 \
--clients 100 --update-hz 10 --duration-sec 300
docker run --rm --env-file loadtest.env cks-loadtest --clients 100
Ramp-up, walk speed, replication distance, worker threads, CSV output, and more are all configurable — see the repository README for the full option table.
Reading the results
A stats line prints on a fixed interval, and a final summary (latency histogram, error-code breakdown) prints on exit:
[stats] clients=100/100 tx=1000 pps 192.5 KB/s | rx=7132 dps 7369.9 KB/s notif=37888/s | lat~5.3ms | errs=0 reconnects=0 hmac_fail=0 send_fail=0
- tx should settle at
clients × update-hzonce the ramp completes. If it lags, the load-generating host is saturated — add threads or hosts. - notif/s is the replication fan-out your app produces: with co-located
clients it grows toward
clients² × update-hz, bounded by your replication--distanceand decay settings. This is usually the number to watch as you scale--clients. - lat~ is a one-way latency estimate from the server timestamp in each notification versus the local clock. It includes clock skew between the hosts, so watch its trend under load rather than its absolute value.
- errs map to the wire protocol's
error codes. A small
burst of
UNAUTHORIZED(7) during ramp-up is the permission warm-up described in Operations; a persistent stream means an entitlement problem (see setup above). - reconnects count server-initiated moves (
COMMAND_RECONNECT). The tool re-assigns those clients automatically. Rising reconnects with dips inclientsunder increasing load is your capacity signal: the environment is asking clients to spread out. That is the point to note as your app's comfortable concurrent-client level for the environment you tested. - Exit code 3 means traffic was sent but nothing was ever received — work through the troubleshooting checklist (address choice, session install, UDP egress).
Add --csv-out stats.csv for a machine-readable per-interval log to graph
alongside your own dashboards.