Lifecycle: HISTORICAL (published KEEP). For the promoted reader route, see Nakama Matchmaking Cheatsheet.
Funday's live query does not follow the
T/Fexamples belowThe upstream
+label.open:Tboolean form shown on this page is not what Funday runs. The production browse query islabel.game:<id> label.open:true -label.queueType:rankedfor casual — no+prefixes, literaltrue. A+-prefixed term silently misses authoritative lobbies in the live runtime. For the current contract see Lobby, Matchmaking & Nakama.
🎮 Nakama Matchmaking
Updated: 2025-12-22
A practical cheat sheet for:
- Matchmaker (find opponents → start a new match)
- Match listing (browse/join existing matches)
- Parties (keep a group together)
🧭 0) Pick the right feature
Matchmaker
- Use when: you want the server to form groups from a pool of active players
- Result: a matchmaker result event containing either a match ID (authoritative) or a token (relayed)
Match listing
- Use when: you want a “server browser” / lobby list
- Result: you list running matches and join one immediately
Parties
- Use when: a pre-made group must enter matchmaking as a unit
Offline matchmaking
- Use when: players are not online at the same time (server-side, storage-index based)
🧩 1) Core primitives (what you must understand)
Ticket
- A unique ID representing a matchmaking request
- A user can have multiple tickets at the same time
- A match found for one ticket does not automatically cancel the other tickets
Properties
- Describe the user submitting the ticket
- Two buckets:
- string properties (region, mode, platform)
- numeric properties (rank, mmr, level)
Query
- Describes what opponents the user wants
- Query terms match against other users’ ticket properties via the
properties.prefix
Matchmaker result
- Delivered asynchronously via realtime socket event
- Includes:
users(matched presences + their properties)- either:
match_id(authoritative matches)token(relayed matches)
Counts
- minCount / maxCount are inclusive of the user submitting the ticket
- the matchmaker will try to form a match at maxCount first, then smaller sizes down to minCount
🔌 2) Client quickstart (nakama-js)
2.1 Create client + socket
Key calls
- Create client:
new Client(serverKey, host, port, useSSL) - Create socket:
client.createSocket(useSSL, trace) - Connect:
await socket.connect(session)
2.2 Join matchmaker pool
Core call
await socket.addMatchmaker(query, minCount, maxCount, stringProperties?, numericProperties?)
Required events to handle
socket.onmatchmakerticket(store ticket so you can cancel)socket.onmatchmakermatched(match found)
2.3 Join the match you were assigned to
Rules
- Matched users do not automatically join
- For relayed matches, the token is short-lived; join immediately
Join patterns
- Relayed:
await socket.joinMatch(null, matched.token) - Authoritative:
await socket.joinMatch(matched.match_id, null)
2.4 Cancel matchmaking
await socket.removeMatchmaker(ticket)- Party:
await socket.removeMatchmakerParty(partyId, ticket)
🔎 3) Query syntax essentials (matchmaker + match listing)
Query term structure
field:value(a space-separated AND)
Operators
- SHOULD (default):
region:europe - MUST:
+region:europe - MUST NOT:
-level:>10
Numeric ranges
rank:>=5 rank:<=10
Regular expressions
mode:/(freeforall|ctf)/
Boosting (ordering)
region:europe^3 region:asia^2 region:africa
Escaping
- Escape these characters in query terms:
+-=&|><!(){}[]^"~*?:\\/ - Space must be escaped too
Boolean fields
- When querying boolean values, use
T/F(true/false)
🎯 4) Matchmaker patterns that actually work
4.1 Skill-based matchmaking (range)
Concept
- Put the user’s skill in numericProperties
- Query for a narrow band of opponent skill
Example query
+properties.skill:>=1450 +properties.skill:<=1550
4.2 Region preference (soft)
Concept
- Prefer same region, but allow fallback
Example query
properties.region:europe^3 properties.region:asia^2 properties.region:africa
4.3 Expanding criteria (don’t spam identical tickets)
Facts
- Repeatedly submitting identical requests won’t magically produce different results
- Prefer widening criteria over time
Practical approach
- Create a strict ticket
- If still unmatched, create a looser ticket
- Cancel old tickets once you commit to a match (or you can get matched twice)
4.4 Matchmaking around blocked users
Concept
- Include blocked user IDs in a string property
- Add a MUST NOT regex query term to exclude tickets where the other side blocked you
Pattern (high-level)
- property:
blocked = "id1 id2 id3" - query term:
-properties.blocked:/.*<my_user_id>.*/
⚙️ 5) Server configuration (matchmaker)
Config keys
- matchmaker.max_tickets
- prevents users from submitting abusive numbers of tickets
- matchmaker.interval_sec
- how often the server attempts to form matches
- lower = faster matching, higher server cost
- matchmaker.max_intervals
- how many intervals to try at maxCount before allowing minCount
- matchmaker.rev_precision / matchmaker.rev_threshold
- optionally enforce bidirectional matching for a short period
Observability
- Matchmaker Stats API
GET /v2/matchmaker/stats(requires auth)- returns:
- ticket_count
- oldest_ticket_create_time
- recent completions (create_time / complete_time)
🧠 6) Authoritative matchmaking: create the match on the server
If you want an authoritative match per matched group
- Register
registerMatchmakerMatchedin runtime - Create a match via
nk.matchCreate(...) - Return the match ID
High-level TypeScript runtime pattern
initializer.registerMatchmakerMatched((ctx, logger, nk, matches) => nk.matchCreate("lobby", {invited: matches}))
Client then
- receives a matchmaker matched event that contains
match_id - calls
socket.joinMatch(match_id, null)
🏷️ 7) Match listing (server browser / lobbies)
Key idea
- Match listing is for joining existing matches immediately
- Matchmaker is for forming a new match from a pool
Label rules
- To query label fields, the label must be JSON
- Keep labels small (max 2KB)
Two ways to search
- Filter (exact label match)
- Query (flexible criteria via query syntax)
Boolean query reminder
+label.open:T(not true/false)
Find-or-create pattern
- List matches for a label/query
- If none exist,
nk.matchCreate(...)
Client-side listing
client.listMatches(session, limit, authoritative, label, minSize, maxSize, query)
Server-side listing
nk.matchList(limit, authoritative, label, minSize, maxSize, query)
Label update best practice
- update label only when a meaningful field changes (open/players/mode)
- avoid updating every tick
🧑🤝🧑 8) Party matchmaking
What parties solve
- ensures a pre-made group stays together during matchmaking
Core flow
- party leader creates party
- leader accepts join requests
- leader calls
addMatchmakerParty(...) - every party member receives matchmaker matched event
- every party member joins the match (typically by token)
Cancellation
- cancel with
removeMatchmakerParty(partyId, ticket)
🧯 9) Common pitfalls (seen in real projects)
-
Joining too late
- relayed match tokens are short-lived → join immediately
-
Forgetting to join at all
- matchmaker assigns opponents, but you still must call
joinMatch
- matchmaker assigns opponents, but you still must call
-
Leaving old tickets around
- a successful match on one ticket does not cancel your other tickets
-
Disconnect during matchmaking
- pending tickets are cancelled when a user disconnects
-
Overly strict queries
- use SHOULD terms or widen criteria over time
-
Bad label/query shape
- label must be JSON to query label fields
- boolean uses
T/F
🧪 10) Debug checklist
Client
- socket connected before calling
addMatchmaker - you see
onmatchmakerticketfire and you store the ticket - you see
onmatchmakermatchedfire - you call
joinMatchimmediately
Server
- check
/v2/matchmaker/statsto see if tickets exist and are completing - confirm server config matchmaker settings are sane
- if using
registerMatchmakerMatched, check logs for match creation errors
🧩 11) Funday notes (optional)
Current Funday production pattern
- Lobby flow uses match listing + “find-or-create” RPC rather than pure matchmaker
Key integration invariant
- after you receive a matchId from API/RPC, you must still join via socket:
await socket.joinMatch(matchId)
🔗 References
Upstream docs (Heroic Labs)
- Matchmaker: https://heroiclabs.com/docs/nakama/concepts/multiplayer/matchmaker/
- Match listing: https://heroiclabs.com/docs/nakama/concepts/multiplayer/match-listing/
- Query syntax: https://heroiclabs.com/docs/nakama/concepts/multiplayer/query-syntax/
Local Funday docs
- Current contract: Lobby, Matchmaking & Nakama