ServicesMatchmaking

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 - streaming pools attempt a match the moment each ticket arrives; batching pools 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 attributes for rules to reference, a priority (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_ids and player_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-duel

The 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_tDSKucUpAuFs

Over 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 get only 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 a NotEqual hard constraint.
  • group_id is recorded, not enforced. A ticket submitted with --group-id docs4-party1 echoed 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/respond and the proposed status exist, but streaming and batch flows both confirm matches directly, so no flow currently produces a proposal - expect 404 proposal_not_found.
  • Expired tickets read as missing. After its TTL a ticket returns 404 ticket_not_found, not a readable expired record (verified with --ttl-seconds 2). TTL must be 1-86400 seconds; 0 is rejected with 400 BAD_REQUEST.
  • Cancellation is owner-only and single-shot. Another user's cancel fails with 403 unauthorized; only waiting/proposed tickets can be cancelled; a repeat fails with 400 ticket_already_cancelled. A cancelled ticket stays readable as "status": "cancelled".
  • metadata.version is reserved. Pools, matches, and rulesets return a service-managed version entry inside metadata even 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

On this page