Matchmaking and lobby testing
Matchmaking is the first service players interact with after login — and the first service to fail when a game launches. Queue storms, party fragmentation under load, and fairness degradation are failure modes that a basic load test will not catch unless the test 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 — 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 experience three distinct failure patterns at high concurrency:
Queue storm. At launch or after a patch, thousands of players hit the matchmaking queue simultaneously. The queue backend — typically a priority queue or Redis sorted set — must handle a write spike that is several times the steady-state enqueue rate. Services that are tuned for steady-state throughput often saturate on the initial burst, causing 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. The visible symptom is that 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, and parties may be split across matches or left in queue while solo players are matched immediately. Fairness under load — ensuring parties are not systematically disadvantaged — requires 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) — so MaxoPerf 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, you need multiple virtual users to act as a unit. The most practical 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 alongside the script entrypoint.
Queue storm configuration
Section titled “Queue storm configuration”For launch-day simulation, use a very 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 produces the sharpest possible load shape. Monitor the matchmaking service’s queue depth metric alongside the MaxoPerf results to correlate enqueue rate with 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 indicates match-formation latency is degrading.- Enqueue error rate. 429 or 503 responses on the
/queuePOST indicate 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 being abandoned in the queue.- Session join errors. A match ticket issued but session join fails indicates a mismatch between the matchmaker’s session-allocation output and the session server’s actual capacity.
Do / don’t
Section titled “Do / don’t”Do:
- Run the matchmaking test alongside 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, not just solo queues.
- Monitor server-side queue depth metrics (e.g. Redis queue length) alongside MaxoPerf results to understand the queue pipeline health, not just the client-visible wait time.
Don’t:
- Use a single static player token for all virtual users — most matchmaking services deduplicate queue entries by player ID and will silently ignore repeat enqueues from the same identity.
- Treat a 30-second average wait time as acceptable without checking the p99 — 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 only validates the full funnel if it verifies that a matched ticket can actually convert 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.