Skip to main content
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 to in_game with the same matchId and the same server connection info.

The Full Flow

1

Players group up in a lobby

One or more players join a lobby (as a party).
2

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

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

Match formed

All matched lobbies atomically claim each other. Status → matched.
5

Game server launches

One dedicated server starts. All matched lobbies get the same matchId and server.ports[].
6

Players connect

Clients read server.ports[0].host:port from their lobby and connect to the game server.

Configuring Matchmaking

Matchmaking modes are defined in your lobby config in the PlayFlow dashboard. Each config can have multiple modes:
{
  "id": "ranked",
  "name": "ranked",
  "enabled": true,
  "timeout": 300,
  "serverSettings": {
    "serverSize": "small",
    "gameBuild": "default"
  },
  "matchmaking": {
    "modes": {
      "1v1": {
        "teams": 2,
        "playersPerTeam": 1,
        "minPlayersPerTeam": 1,
        "timeout": 60,
        "maxPartySize": 1,
        "allowBackfill": false,
        "matchmakingRules": [
          { "type": "difference", "field": "mmr", "maxDifference": 100 }
        ]
      },
      "5v5": {
        "teams": 2,
        "playersPerTeam": 5,
        "minPlayersPerTeam": 3,
        "timeout": 120,
        "maxPartySize": 5,
        "allowBackfill": true
      }
    }
  }
}
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

POST /v3/lobbies/ranked/me/matchmaking
x-player-id: host_player
Content-Type: application/json

{
  "mode": "5v5"
}
Response — lobby transitions to in_queue with queue stats:
{
  "status": "in_queue",
  "matchmaking": {
    "mode": "5v5",
    "startedAt": "2026-04-08T20:30:00Z",
    "queueStats": {
      "playersSearching": 42,
      "lobbiesInQueue": 8,
      "avgWaitSeconds": 12.3
    }
  },
  ...
}
Only the host can start matchmaking. Non-hosts get 403 Not host.

When a Match Is Found

When the system matches your lobby with others, the SSE stream emits an update:
event: lobby_updated
data: {
  "status": "in_game",
  "matchId": "abc-123",
  "server": {
    "instanceId": "srv-xyz",
    "status": "launching",
    "region": "us-east",
    "ports": [{"name":"game","host":"203.0.113.42","port":7770,"protocol":"udp"}]
  },
  ...
}
All matched lobbies get the same matchId and same server. Clients connect to server.ports[0].host:port.

Cancel Matchmaking

DELETE /v3/lobbies/ranked/me/matchmaking
x-player-id: host_player
Lobby returns to waiting.

Team Modes

Symmetric Teams (Traditional)

Most competitive games. N teams of M players, everyone equal.
{
  "teams": 2,
  "playersPerTeam": 5,
  "minPlayersPerTeam": 3
}
  • teams — how many teams to form
  • playersPerTeam — target size (matchmaker tries to fill to this)
  • minPlayersPerTeam — minimum required to start (for partial matches)
Examples:
ModeConfig
1v1 rankedteams: 2, playersPerTeam: 1
2v2 duosteams: 2, playersPerTeam: 2
5v5 competitiveteams: 2, playersPerTeam: 5
100-player FFAteams: 1, playersPerTeam: 100, minPlayersPerTeam: 20
50-squad battle royaleteams: 50, playersPerTeam: 2, minTeams: 10

Asymmetric Teams (Role-Based)

For games like Dead by Daylight (1 killer vs 4 survivors) or Evolve (1 monster vs 4 hunters):
{
  "teams": 2,
  "playersPerTeam": 3,
  "minPlayersPerTeam": 2,
  "teamComposition": {
    "monsters": { "boss": 1 },
    "hunters": { "medic": 1, "assault": 2, "support": 1 }
  },
  "excludedFromQueue": ["spectator"]
}
Players specify their role in state.role:
PATCH /v3/lobbies/hunt/me
x-player-id: p1

{ "state": { "role": "boss" } }
Or a preference list (higher priority first):
{ "state": { "role": ["medic", "support", "fill"] } }
The matchmaker:
  • Places players with the most specific role first (boss before fill)
  • 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)

Skill-Based Matchmaking

Add matchmaking rules to filter candidates by player attributes:
{
  "matchmakingRules": [
    { "type": "difference", "field": "mmr", "maxDifference": 100 }
  ]
}
Each rule checks: |lobbyA.{field} - lobbyB.{field}| <= maxDifference. Lobby state is automatically averaged across eligible players to compute each lobby’s value. So if a party has players with MMR 1400 and 1600, the lobby’s MMR is 1500.
Works with any numeric field in state. Common choices: mmr, elo, level, rank, kd_ratio.

Auto-Expanding Buckets

The hardest part of skill matchmaking is balancing quality vs wait time. PlayFlow handles this automatically:
As players wait longer, the acceptable range grows.
Formula: effectiveMax = maxDifference × (1 + longestWaitTime / modeTimeout) Using maxDifference: 100, timeout: 60s:
Wait timeEffective range
0s±100 MMR (strict)
15s±125 MMR
30s±150 MMR
60s±200 MMR
120s±300 MMR
No extra configuration. Just set maxDifference and timeout — the expansion is derived. This is how every competitive matchmaker (Riot, Valve, Blizzard) works — they just never made you configure it.

Multiple Rules

Add multiple rules and all must pass:
{
  "matchmakingRules": [
    { "type": "difference", "field": "mmr", "maxDifference": 100 },
    { "type": "difference", "field": "level", "maxDifference": 5 }
  ]
}

Region Matching

Players specify their acceptable regions in state (or PlayFlow falls back to the lobby’s region):
PATCH /v3/lobbies/ranked/me

{ "state": { "regions": ["us-east", "us-west"] } }
The matchmaker:
  1. Intersects all matched lobbies’ region lists
  2. Within the intersection, counts weighted votes (1/(index+1) by preference order)
  3. Picks the highest-voted region for the server
If lobbies have no overlap, the match doesn’t form — they’re considered incompatible.

Queue Stats

While in in_queue, your lobby response includes live queue stats:
{
  "matchmaking": {
    "mode": "5v5",
    "startedAt": "...",
    "queueStats": {
      "playersSearching": 42,
      "lobbiesInQueue": 8,
      "avgWaitSeconds": 12.3
    }
  }
}
Via SSE, you get push updates every 10 seconds:
event: queue_stats
data: { "playersSearching": 45, "lobbiesInQueue": 9, "avgWaitSeconds": 11.8 }
Use this to show your players ”12s estimated wait” or “42 searching now” in your UI.

Backfill

For long-running persistent games (e.g., battle royale with late-join):
{
  "allowBackfill": true
}
When backfill is enabled, the matchmaker will add new lobbies to existing in-progress games that have open slots — not just create new matches. Players join the running server and jump right into the action.

Party Size Limits

Prevent a 5-stack from queueing for a 1v1:
{
  "maxPartySize": 1
}
Lobbies with more than maxPartySize eligible players are rejected from queueing.

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).
Concurrency safety: The matchmaker uses Postgres 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

StatusCause
400 Mode not foundThe mode you passed isn’t defined in your lobby config
400 Party size exceededYour lobby has more players than maxPartySize
400 Already in queueLobby is already in in_queue status
403 Not hostNon-host tried to start/cancel matchmaking
409 Invalid stateLobby is in_game or otherwise not in waiting

Full Example: Ranked 1v1

# Host creates a lobby (with MMR in state)
curl -X POST "https://api.computeflow.cloud/api/v3/lobbies/ranked" \
  -H "api-key: pfclient_..." \
  -H "x-player-id: p1" \
  -d '{"name":"1v1","maxPlayers":1,"state":{"mmr":1500}}'

# Host queues for matchmaking
curl -X POST "https://api.computeflow.cloud/api/v3/lobbies/ranked/me/matchmaking" \
  -H "api-key: pfclient_..." \
  -H "x-player-id: p1" \
  -d '{"mode":"1v1"}'

# Separately, another player does the same — they match automatically
# Both get the same matchId and server info
curl "https://api.computeflow.cloud/api/v3/lobbies/ranked/me" \
  -H "api-key: pfclient_..." \
  -H "x-player-id: p1"
# → { "status": "in_game", "server": { "ip": "...", "port": 7770 } }

Real-time Events

Stream matchmaking progress and queue stats via SSE.

API Reference

Full schemas for the matchmaking endpoints.