PlayFlow’s matchmaking system finds compatible opponents for your players and launches a shared game server. It supports symmetric teams, asymmetric roles, skill-based filtering, region preferences, and handles concurrency safely. A matchmaking game is just a group of lobbies pointing to the same server. When two lobbies match, they both transition toDocumentation Index
Fetch the complete documentation index at: https://docs.playflowcloud.com/llms.txt
Use this file to discover all available pages before exploring further.
in_game with the same matchId and the same server connection info.
The Full Flow
Host starts matchmaking
POST /v3/lobbies/{config}/me/matchmaking with a mode name.Lobby status → in_queue. matchmaking.mode populates. Queue stats begin streaming via SSE.System searches for compatible lobbies
Other lobbies in the same config + mode are evaluated. Rules applied (MMR, region, party size). Candidates sorted by wait time.
Game server launches
One dedicated server starts. All matched lobbies get the same
matchId and server.network_ports[].Configuring Matchmaking
Matchmaking modes are defined in your lobby config in the PlayFlow dashboard. Each config can have multiple modes:No config? No problem. If you don’t set up matchmaking in the dashboard, the
/me/matchmaking endpoint returns a 400 asking you to configure modes. All lobby operations still work without config (they use defaults).Starting Matchmaking
in_queue with queue stats:
When a Match Is Found
When the system matches your lobby with others, the SSE stream emits an update:matchId and same server. Clients connect to server.network_ports[0].host:external_port. The server block is the same shape as GET /v3/servers/{instance_id} — server is a primitive the lobby wraps around.
Cancel Matchmaking
waiting.
Match Confirmation
CS2-style “Accept Match” flow. For ranked or competitive modes, you often want players to accept a match before the server launches. PlayFlow supports this natively — addmatchConfirmation to the mode:
How it works
- Matchmaker finds a match → all lobbies transition
in_queue→match_found. Server is NOT launched yet. - Each lobby’s response now includes a confirmation block:
- Players accept or decline:
- Accept:
POST /v3/lobbies/{config}/me/confirm-match - Decline:
DELETE /v3/lobbies/{config}/me/confirm-match
- Accept:
- When every matched lobby accepts → server launches →
in_game. - If any lobby declines OR the deadline passes → all participating lobbies return to
waiting(matchmaking state cleared). Players re-queue when ready.
Who confirms? Any player in a lobby can accept or decline on behalf of their party. Parties confirm as a unit (one click per party, not per player).
Full flow over SSE
The client sees the whole flow in real-time:Rematch (keep the same lobby)
When a match ends, the host can recycle the lobby for another round instead of making players re-queue:- Transitions lobby from
in_game→waiting - Stops the current game server
- Keeps all players, invite code, and settings
- Ready for
/me/startor/me/matchmakingagain
waiting the same way — no manual call needed.
Team Modes
Symmetric teams
Traditional team structure — most competitive games. N teams of M players, everyone equal.teams— how many teams to formplayersPerTeam— target size (matchmaker tries to fill to this)minPlayersPerTeam— minimum required to start (for partial matches)
| Mode | Config |
|---|---|
| 1v1 ranked | teams: 2, playersPerTeam: 1 |
| 2v2 duos | teams: 2, playersPerTeam: 2 |
| 5v5 competitive | teams: 2, playersPerTeam: 5 |
| 100-player FFA | teams: 1, playersPerTeam: 100, minPlayersPerTeam: 20 |
| 50-squad battle royale | teams: 50, playersPerTeam: 2, minTeams: 10 |
Asymmetric teams
Role-based team structure for games like Dead by Daylight (1 killer vs 4 survivors) or Evolve (1 monster vs 4 hunters):state.role:
- Places players with the most specific role first (
bossbeforefill) - Keeps party members together on the same team
- Uses
"fill"as a wildcard (assigns wherever there’s an open slot) - Excludes spectators (or any role in
excludedFromQueue)
Matchmaking Rules
Rules decide which lobbies are allowed to match with each other. Add them to a mode underrules:
gameVersion) are propagated only if all party members agree on the same value.
The 5 rule types
difference
Numeric tolerance with auto-expanding buckets. Skill matching.
equals
Both sides must share a field value (version, map).
not_equals
Values must differ (anti-rematch).
region
Acceptable-region overlap (≥ N shared).
expression
CEL escape hatch for custom logic.
Skill matching
Thedifference rule pairs players whose state differs by no more than a threshold, with buckets that widen over time.
|lobbyA.mmr - lobbyB.mmr| <= maxDifference. Works with any numeric field: mmr, elo, level, rank, kd_ratio.
The magic: as players wait longer, the bucket grows automatically. No extra config.
Formula: effectiveMax = maxDifference × (1 + longestWaitTime / modeTimeout)
Using maxDifference: 100, timeout: 60s:
| Wait time | Effective range |
|---|---|
| 0s | ±100 MMR (strict) |
| 30s | ±150 MMR |
| 60s | ±200 MMR |
| 120s | ±300 MMR |
Equals rule
Theequals rule pairs players only when a specific state field matches exactly on both sides.
lobbyA.gameVersion == lobbyB.gameVersion. Use for: game version pinning, map selection, platform gating.
Not-equals rule
Thenot_equals rule pairs players only when a state field has different values on each side.
Region rule
Theregion rule pairs lobbies only when their acceptable-region lists overlap by at least minOverlap entries.
minOverlap entries in their acceptableRegions list. Players set regions via their state:
acceptableRegions is empty, the lobby is “open to any region” (wildcard — always passes).
Expression rule
Theexpression rule is a CEL escape hatch for logic the other four primitives don’t cover. Uses CEL (Common Expression Language).
a and b (the two lobby tickets) and ctx (mode config). Each ticket exposes id, partySize, waitSeconds, region, acceptableRegions, state, metadata.
More CEL examples:
Multiple rules — all must pass
Stack rules together:Dynamic Version Selection
Shipping a new game version usually means a rolling update — stop old matches, cut the fleet over to the new build, and hope nothing breaks for players mid-session. That’s infrastructure overhead you pay on every release. SetuseVersionFromState: "gameVersion" on a mode. Your client sends gameVersion in its player state when queueing, an equals rule on the same field keeps matched players on the same version, and the matchmaker launches whichever build has that name. No rolling updates, multiple versions live simultaneously in the same project.
Config
useVersionFromState maps to a build’s name (e.g. 1.2.3), not its numeric version. serverSettings.gameBuild acts as the fallback when the field is missing from every matched player’s state.
Unity SDK
Send the version in player state before you queue:Prerequisites
- A build whose
namematches the state value must exist and beready - Every matched player must send the field in their state — add a client-side check with a sensible fallback
- An
equalsrule on the same field is required (the dashboard and API reject the config otherwise)
Failure modes
| Condition | Outcome |
|---|---|
| Field missing from state | Match fails, players return to waiting |
Build with that name doesn’t exist or isn’t ready | Match fails, players return to waiting |
No equals rule on the same field | Config save rejected (400) |
Region Matching
Players specify their acceptable regions in state (or PlayFlow falls back to the lobby’s region):- Intersects all matched lobbies’ region lists
- Within the intersection, counts weighted votes (1/(index+1) by preference order)
- Picks the highest-voted region for the server
Queue Stats
While inin_queue, your lobby response includes live queue stats:
Backfill
For long-running persistent games (e.g., battle royale with late-join):Party Size Limits
Prevent a 5-stack from queueing for a 1v1:maxPartySize eligible players are rejected from queueing.
Matchmaking Timeout
Thetimeout field on a matchmaking mode caps how long a player will wait in queue before the system gives up.
waiting status with an explanatory event over SSE — they are not re-queued automatically. Players must explicitly press matchmake again.
Typical values:
30— casual / quick play60–120— ranked180+ — large FFAs or niche modes where the candidate pool is thin
Battle Royale
Battle royale modes are not a separate feature — they are just a particular shape of the existingteams / playersPerTeam fields. There is nothing special to enable on the backend.
Squad BR uses the Teams format: many small squads fight in one match. A { "teams": 25, "playersPerTeam": 4 } mode fills one 100-player server from 25 four-player lobbies.
Solo BR uses the Free-for-all format: { "teams": 100, "playersPerTeam": 1 } fills a 100-player match from 100 solo lobbies.
Anchor-pairwise matching
When N lobbies come together to form one match, rules are evaluated pairwise against the anchor (the first lobby in the queue), not against a running average. For a 25-squad BR that means 24 anchor-vs-candidate evaluations — each candidate must be compatible with the anchor. This matters because two admitted lobbies can legally be up to2 × maxDifference apart from each other, even though each is within maxDifference of the anchor.
Rule recommendations
- Skill (
differenceonmmr) — works, but tunemaxDifferenceon the conservative side. Remember two admitted squads can be2 × maxDifferenceapart. equalsongameVersion— every lobby must match the anchor exactly. Recommended.region— critical for BR. One cross-continent squad ruins the server for everyone.
Party semantics reminder
Numeric fields in a lobby’sstate (like mmr) are averaged within a party when it joins matchmaking together. Across parties, each party carries its own averaged value — the matchmaker compares those averages against the anchor.
Example: 25-squad BR
How Matchmaking Runs
Matchmaking runs inline on queue entry. When lobby B calls
POST /me/matchmaking, the system immediately scans for compatible lobbies. If A is already queued, B matches with A within ~100ms. No polling loops, no worker delay.A QStash-driven cron runs every 60s as a safety net (catches edge cases like simultaneous-entry races).FOR UPDATE SKIP LOCKED + a claim-then-create pattern. Two simultaneous match attempts can never claim the same lobbies or launch duplicate servers.
Error Cases
| Status | Cause |
|---|---|
400 Mode not found | The mode you passed isn’t defined in your lobby config |
400 Party size exceeded | Your lobby has more players than maxPartySize |
400 Already in queue | Lobby is already in in_queue status |
403 Not host | Non-host tried to start/cancel matchmaking |
409 Invalid state | Lobby is in_game or otherwise not in waiting |
Full Example: Ranked 1v1
Real-time Events
Stream matchmaking progress and queue stats via SSE.
API Reference
Full schemas for the matchmaking endpoints.