Matchmaking and lobby testing
Matchmaking is the first service players use after login, and the first service to fail when a game launches. A basic load test misses queue storms, party fragmentation under load and fairness degradation unless it models the matchmaking workflow explicitly. This page shows you how.
Before you start
Section titled “Before you start”- A game server load test has established the game server’s concurrent-session capacity. Matchmaking is the funnel that fills those sessions, so test it against the same capacity ceiling.
- Know your matchmaking API surface: the endpoint that submits a match request, the polling or WebSocket callback that returns a match assignment, and the session-join endpoint.
Why matchmaking fails under load
Section titled “Why matchmaking fails under load”Matchmaking services fail in three distinct ways at high concurrency:
Queue storm. At launch or after a patch, thousands of players hit the matchmaking queue at once. The queue backend (typically a priority queue or Redis sorted set) must handle a write spike several times the steady-state enqueue rate. Services tuned for steady-state throughput often saturate on the initial burst and return 429 or timeout responses.
Match-formation latency drift. When the queue is full, the matchmaker must evaluate thousands of candidates per second to form balanced matches. Under load, this computation competes with enqueue/dequeue I/O. You see wait times grow super-linearly with queue depth: a queue at 10× normal depth can have 50× normal wait times.
Party fragmentation. Party (group) matchmaking is harder than solo matchmaking. The system must keep a group of 2–5 players together and find a match that fits them as a unit. Under load, the combinatorial search space grows. Parties may be split across matches or left in queue while solo players are matched immediately. To check fairness under load (parties are not systematically disadvantaged), you need a specific test pattern.
Modeling the matchmaking workflow in k6
Section titled “Modeling the matchmaking workflow in k6”A matchmaking load test has three phases per virtual user:
- Enqueue: POST to the matchmaking endpoint to join the queue.
- Wait and poll: GET or WebSocket-listen for a match assignment, with realistic backoff.
- Join session: POST to the session-join endpoint with the match token.
# taurus.yml — matchmaking queue stormexecution: - executor: k6 concurrency: 1000 # 1 000 players queuing simultaneously ramp-up: 30s # fast ramp to simulate launch storm hold-for: 5m scenario: matchmaking
scenarios: matchmaking: script: matchmaking.jsimport http from 'k6/http';import { check, sleep } from 'k6';import { Trend } from 'k6/metrics';
const queueWaitTime = new Trend('matchmaking_wait_ms');const matchFoundRate = { matched: 0, timed_out: 0 };
export default function () { const authHeader = { headers: { Authorization: `Bearer ${__ENV.PLAYER_TOKEN}` } };
// 1. Enqueue const enqueueRes = http.post( 'https://matchmaking.staging.example.com/v1/queue', JSON.stringify({ mode: 'ranked-5v5', region: 'us-east' }), { headers: { ...authHeader.headers, 'Content-Type': 'application/json' } } );
check(enqueueRes, { 'enqueue 202': (r) => r.status === 202 }); if (enqueueRes.status !== 202) return;
const ticketId = enqueueRes.json('ticket_id'); const enqueueTs = Date.now();
// 2. Poll for match (up to 60 s) let matched = false; for (let i = 0; i < 30; i++) { sleep(2); const statusRes = http.get( `https://matchmaking.staging.example.com/v1/queue/${ticketId}`, authHeader );
if (statusRes.status === 200 && statusRes.json('status') === 'matched') { queueWaitTime.add(Date.now() - enqueueTs); matched = true;
// 3. Join session const joinRes = http.post( 'https://matchmaking.staging.example.com/v1/sessions/join', JSON.stringify({ match_token: statusRes.json('match_token') }), { headers: { ...authHeader.headers, 'Content-Type': 'application/json' } } ); check(joinRes, { 'session join 200': (r) => r.status === 200 }); break; } }
check(null, { 'match found within 60s': () => matched });}The custom matchmaking_wait_ms trend metric tracks the queue wait time per virtual
player. Set a threshold on it, e.g. p(95)<30000 (30 s at p95). MaxoPerf then fails
the run automatically if the matchmaker is too slow under load.
Party matchmaking test
Section titled “Party matchmaking test”To model party (group) matchmaking, several virtual users have to act as a unit. The simplest approach in k6 is to pre-generate party tokens server-side (via a setup fixture or a small admin script) and feed them as test data:
import { SharedArray } from 'k6/data';
const parties = new SharedArray('parties', function () { // Each row: { party_token: "...", size: 4 } return JSON.parse(open('./parties.json'));});
export default function () { const party = parties[__VU % parties.length]; // Use party.party_token to enqueue the whole party as one unit const res = http.post( 'https://matchmaking.staging.example.com/v1/queue', JSON.stringify({ mode: 'ranked-5v5', party_token: party.party_token }), { headers: { 'Content-Type': 'application/json' } } ); check(res, { 'party enqueue 202': (r) => r.status === 202 });}Upload parties.json as a Test asset in MaxoPerf next to the script entrypoint.
Queue storm configuration
Section titled “Queue storm configuration”For launch-day simulation, use a fast ramp (30 seconds from zero to full VU count) to reproduce the “game just went live” traffic shape:
execution: - executor: k6 concurrency: - const: 0 duration: 0s - const: 5000 # 5 000 concurrent match requests duration: 30s # instant storm hold-for: 3m scenario: matchmakingThis gives the sharpest possible load shape. Watch the matchmaking service’s queue depth metric next to the MaxoPerf results to match enqueue rate against queue depth growth.
What to look for in the results
Section titled “What to look for in the results”matchmaking_wait_msp95. This is the headline metric. Growth across the hold phase means match-formation latency is degrading.- Enqueue error rate. 429 or 503 responses on the
/queuePOST mean the backend is rate-limiting or overloaded at the ingestion layer. match found within 60scheck failure rate. Any virtual player that times out in the 60-second poll window counts as a failed check. A rising failure rate means players are left behind in the queue.- Session join errors. When a match ticket is issued but the session join fails, the matchmaker’s session-allocation output does not match the session server’s real capacity.
Do / don’t
Section titled “Do / don’t”Do:
- Run the matchmaking test together with a game server load test to validate end-to-end capacity. Matchmaking capacity only matters if the game server can handle the matched sessions.
- Include party tokens in your data set to test group matchmaking as well as solo queues.
- Watch server-side queue depth metrics (e.g. Redis queue length) next to MaxoPerf results. They show the health of the queue pipeline, which the client-visible wait time alone does not.
Don’t:
- Use a single static player token for all virtual users. Most matchmaking services deduplicate queue entries by player ID and silently ignore repeat enqueues from the same identity.
- Treat a 30-second average wait time as acceptable and skip the p99 check. A small fraction of players stuck in a 5-minute queue destroys retention even if the median is fine.
- Skip the session-join step. The test validates the full funnel only if it checks that a matched ticket converts to a live session.
Where to go next
Section titled “Where to go next”- Game server load testing: verify the server handles the sessions matchmaking creates.
- Launch spike and soak: combine matchmaking and game server into a launch-day scenario.
- Cookbook: CSV data entities: use player/party data files in MaxoPerf tests.
- Test types: Spike test: the spike test pattern behind a queue storm.