Skip to content

Core Concepts

Six concepts describe the whole system. The rest of the documentation builds on them.

flowchart LR
    T[Ticket<br/>one player or party] --> P[(Pool<br/>everyone waiting)]
    P --> E{{Engine<br/>runs every tick}}
    E -->|match formed| M[Match<br/>teams + host]
    E -.->|nothing yet| P
    M --> O[Outcome<br/>pushed to the game backend]

A ticket is one player, or one party, waiting to be matched. It is the unit of matchmaking, and the game backend creates it with an ID of its choosing.

A party is one ticket containing several players, never several tickets. The engine will not split a ticket across teams, so “we queued together, we play together” is a guarantee and not a preference.

A ticket carries two payloads with opposite purposes:

  • engine_input is what the engine reads: players, skill ratings, measured latencies, attributes.
  • metadata is not read by anything. IVK Match never looks inside it and returns it verbatim on every outcome about that ticket. Storing session and party IDs here removes the need for a lookup table keyed by ticket ID.

A queue groups players who want the same kind of match. It is also the unit of configuration: one queue is described completely by one declaration covering team shape, latency budgets, skill tolerance, and attribute rules.

A ruleset is a named matching configuration inside a queue. One queue can run several side by side (different team sizes, playlists, or skill brackets), and each ticket picks which rulesets it competes in. Most integrations start with one queue and one ruleset.

See Queue Configuration.

The pool is every ticket currently waiting in a queue, and it is the engine’s working set.

Pool size drives match quality more than any configurable parameter. More people waiting means more candidates to choose between, which means closer skill matches and better connections. A thinly-populated queue produces worse matches than a busy one under identical configuration, and tuning does not change that.

The engine runs on a tick, once per second by default. Matching is not event-driven: a ticket arriving does not trigger a matching attempt.

The engine, Nemesis, is stateful and iterative. It does not solve the pool from scratch each tick. It keeps working on the matches it is assembling and improves them tick by tick: adding a player here, swapping one there, deciding a group has come together well enough to ship. This has two consequences:

  • A match does not form the instant enough players are present. The engine will hold a nearly-complete group for a few more ticks if the pool suggests it can do better.
  • The engine paces itself against the pool. When the pool is thin it matches conservatively, because every match it forms removes candidates the next one could have used.

Every configurable parameter is either a hard filter (may these players share a match at all) or a preference used to score the options the filters allowed. Weights only mean anything relative to each other.

A match is the engine’s output: a set of tickets, divided into teams, with a chosen host to run on. Its ID is a UUID and the natural key for the game session created from it.

IVK Match never interprets host identifiers. Whatever strings the tickets use as latency keys come back as the match’s preferred host, plus every alternative acceptable to all tickets in the match. When those are the identifiers the provisioning layer already uses, host selection is a pass-through instead of a translation layer. The acceptable list then doubles as a fallback when provisioning fails.

An outcome is anything IVK Match reports asynchronously: a match formed, tickets expired, tickets were removed. Outcomes are pushed to the game backend.

Three properties shape how the outcome consumer is written.

A formed match is a commitment. By the time the outcome exists, those tickets have already left the pool and those players are no longer matchmaking. If the outcome is dropped, they remain on a screen that will never advance. If the game backend cannot honour a match, most often because provisioning failed, it must compensate by returning the tickets to the pool.

Delivery is at-least-once. Every sink can deliver the same outcome twice: after a retry, a reconnect, or a restart between write and acknowledgement. Keying session creation on the match ID makes duplicates harmless.

Ordering is per queue. Outcomes for one queue arrive in the order they happened. Independent queues are independent.

See Outcome Delivery.

Queue wait and server provisioning dominate; the engine contributes at most one tick. The breakdown is in Integration Flow § Where the time goes.

Integration Flow walks the full sequence, and the guides beside it cover cancellation, provisioning failure, timeouts, and backfill. Every term used across the documentation is defined in the Glossary.