Skip to content

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.

  • 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.

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.

A matchmaking load test has three phases per virtual user:

  1. Enqueue: POST to the matchmaking endpoint to join the queue.
  2. Wait and poll: GET or WebSocket-listen for a match assignment, with realistic backoff.
  3. Join session: POST to the session-join endpoint with the match token.
# taurus.yml — matchmaking queue storm
execution:
- 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.js
matchmaking.js
import 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.

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.

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: matchmaking

This 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.

  • matchmaking_wait_ms p95. 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 /queue POST mean the backend is rate-limiting or overloaded at the ingestion layer.
  • match found within 60s check 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:

  • 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.