Matchmaking
Skill-based player matching: pools in streaming or batch mode, user-scoped tickets with JSON attributes, priority, and TTL, rulesets that combine hard constraints with weighted soft preferences, and confirmed matches.
When to reach for it: ranked queues, quick-match lobbies, pairing players or agents by attribute compatibility - same region, similar skill - with automatic expiry of stale requests.
When not to: bracket progression after players are matched belongs to Tournament; fair queueing into a capacity-limited resource belongs to Waiting Room; ranked standings belong to Leaderboard.
Concepts
- Pool - a matchmaking queue with a mode, a capacity, and optionally a ruleset. Capacity caps waiting tickets only (default 1000).
- Mode -
streamingpools attempt a match the moment each ticket arrives;batchingpools accumulate tickets and a background worker matches them on an interval (15 seconds by default). - Ticket - one user's request to be matched, owned by the JWT subject.
It carries free-form JSON
attributesfor rules to reference, apriority(higher sorts first, age breaks ties), and a TTL (default 300 seconds). Statuses:waiting,proposed,matched,expired,cancelled. - Ruleset - named hard constraints (must hold) plus weighted soft preferences (score the candidates), written as JSON expression trees. Own page: Rulesets and constraints.
- Match - a confirmed grouping of tickets, carrying
ticket_idsandplayer_ids. The current matcher builds 1v1 pairs; with a scoring ruleset it can also consider four-ticket groups.
Pools
snug matchmaking pool create -p docs4-duel -n "Docs4 Duel" -m streaming -c 100 -d "1v1 quick match"
snug matchmaking pool create -p docs4-mixer -n "Docs4 Mixer" -m batching
snug matchmaking pool get -p docs4-duel
snug matchmaking pool update -p docs4-duel -c 200 # name, description, capacity, ruleset, metadata
snug matchmaking pool list -l 10
snug matchmaking pool delete -p docs4-duelThe CLI's pool list exposes only --limit/--offset; the HTTP endpoint
speaks the full shared search grammar -
verified with ?q=Mixer (pool name) and ?filter=mode:eq:batching.
A pool with waiting tickets refuses deletion with
400 invalid_pool_configuration ("Cannot delete pool with active
tickets") - cancel or drain them first. A pool's mode cannot be changed by
update.
Tickets
snug matchmaking ticket submit -p docs4-duel -a '{"skill": 1480, "region": "us-west"}' --priority 10
snug matchmaking ticket get -t ticket_tDSKucUpAuFs
snug matchmaking ticket cancel -t ticket_tDSKucUpAuFsOver HTTP, submission is a POST to the pool; captured live:
curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"attributes": {"skill": 1470, "region": "us-west"}, "priority": 10}' \
http://localhost:4000/api/v1/matchmaking/pools/docs4-duel/tickets{
"status": 201,
"msg": "Created",
"data": {
"ticket": {
"ticket_id": "ticket_VuDaYFSWXGqP",
"pool_id": "docs4-duel",
"user_id": "docs-wave",
"attributes": { "region": "us-west", "skill": 1470 },
"priority": 10.0,
"created_at": "2026-08-28T13:32:23.727196Z",
"expires_at": "2026-08-28T13:37:23.727196Z",
"status": "waiting"
}
}
}Tickets always belong to the authenticated user - there is no
submit-on-behalf-of. A user may hold at most 3 concurrent tickets across
all pools (400 too_many_tickets_per_user), and a full pool rejects
submits with 400 pool_capacity_exceeded.
Watching a match form
In a streaming pool, matching runs during each submit - verified with two
identities: the first ticket waits, and the instant a compatible second
ticket arrives, both flip to matched:
snug matchmaking ticket submit -p docs4-duel -a '{"skill": 1480, "region": "us-west"}'
# second user:
snug matchmaking ticket submit -p docs4-duel -a '{"skill": 1520, "region": "us-west"}'
snug --output json matchmaking ticket get -t ticket_tDSKucUpAuFs
# "status": "matched"The match itself is fetched by id:
snug --output json matchmaking match get -m match_eJYmvJcWAawP{
"match_data": {
"match_id": "match_eJYmvJcWAawP",
"pool_id": "docs4-duel",
"ticket_ids": ["ticket_LDXdMwJffXBF", "ticket_tDSKucUpAuFs"],
"player_ids": ["docs4-other", "docs-wave"],
"created_at": "2026-08-28T13:29:41.197040Z",
"status": "confirmed",
"metadata": { "version": 1 }
}
}With no ruleset, any two tickets pair FIFO by priority; a ruleset gates
and scores candidates (Rulesets and constraints shows a
same-region constraint keeping incompatible tickets waiting). In a batch
pool the same submits stay waiting until the next worker cycle -
verified both flipped to matched within 20 seconds.
Behaviors and gotchas
Each of these was reproduced live:
- The submit response is a pre-match snapshot. A streaming match can
form during the submit call, yet the response still shows
waiting- it was captured before the matcher ran. Re-read the ticket. - A matched ticket does not tell you its match id. Tickets carry no
match reference and there is no list-matches endpoint, so
match getonly works with an id obtained elsewhere (today: server logs). - Nothing prevents self-matching. Two tickets from the same user in
one streaming pool matched each other immediately. Rules only see
attributes, so to forbid it, put your own identity into the attributes and add aNotEqualhard constraint. group_idis recorded, not enforced. A ticket submitted with--group-id docs4-party1echoed the group on every read, but the matcher paired it with a stranger's solo ticket - parties are not kept together.- Proposals are modeled but never minted.
proposal get/respondand theproposedstatus exist, but streaming and batch flows both confirm matches directly, so no flow currently produces a proposal - expect404 proposal_not_found. - Expired tickets read as missing. After its TTL a ticket returns
404 ticket_not_found, not a readableexpiredrecord (verified with--ttl-seconds 2). TTL must be 1-86400 seconds; 0 is rejected with400 BAD_REQUEST. - Cancellation is owner-only and single-shot. Another user's cancel
fails with
403 unauthorized; onlywaiting/proposedtickets can be cancelled; a repeat fails with400 ticket_already_cancelled. A cancelled ticket stays readable as"status": "cancelled". metadata.versionis reserved. Pools, matches, and rulesets return a service-managedversionentry insidemetadataeven when you sent none - treat that key as bookkeeping.
Limits and configuration
Pool capacity defaults to 1000 waiting tickets, ticket TTL to 300 seconds,
the batch interval to 15 seconds, and the per-user ticket limit to 3 - all
tunable via the MATCHMAKING_* variables in the
Matchmaking CONFIG reference.
Reference
- Matchmaking API - every endpoint, callable
- Rulesets and constraints - the rule expression grammar
- Related: Tournament for brackets after the match, Leaderboard for standings, Waiting Room for fair-access queues