Integration Flow
End-to-end sequences from a player pressing “Play” to that player connecting to a server.
- Participants
- Model A: backend-driven provisioning
- Cancellation
- Provisioning failure and compensation
- Search timeout
- Backfill
- Ticket lifecycle
- Model B: matchmaker-driven provisioning
- Integration checklist
Participants
Section titled “Participants”| Participant | Role |
|---|---|
| Player | Game client. Presses Play, waits, receives connect info. |
| Game Backend | The customer’s service. Owns player identity, skill ratings, session state. The only thing that talks to IVK Match. |
| IVK Match | The matchmaker — the managed service, or a self-hosted container at a later stage. Holds the ticket pool, forms matches, emits outcomes. |
| Orchestrator | The platform’s gameserver provisioning API. |
| Game Server | The provisioned dedicated server process. |
One rule underpins everything: IVK Match never talks to the player, and the player never talks to IVK Match. The game backend is always in the middle, which keeps authentication, anti-cheat, skill lookup, and rate limiting where they belong.
The diagrams below name operations by their gRPC name (CreateTicket, CancelTicket, and
so on). Every one of them is equally available as a plain HTTP/JSON call, and the flows
are identical either way. See
Matchmaking API reference.
Model A: backend-driven provisioning
Section titled “Model A: backend-driven provisioning”The recommended integration. Works with the current build.
sequenceDiagram
autonumber
actor P as Player
participant GB as Game Backend
participant IM as IVK Match
participant OR as Orchestrator
participant GS as Game Server
P->>GB: queue_matchmaking(playlist, party)
Note over GB: Look up MMR, measure/read latencies,<br/>resolve platform + crossplay prefs
GB->>GB: ticket_id = uuid4()
GB->>IM: CreateTicket(ticket_id, queue_id, ruleset_ids,<br/>engine_input, metadata)
IM-->>GB: ticket_id (ack, ticket is in the pool)
GB-->>P: queued (ticket_id)
Note over IM: Every tick (default 1s) the engine improves<br/>the matches it is assembling from the pool
IM->>IM: match formed, tickets leave the pool
IM->>GB: outcome: match.created<br/>{ match_id, tickets[metadata], engine_output }
Note over GB: Decode engine_output →<br/>teams + host.preferred
GB->>OR: CreateGameServer(host=preferred, match_id, players)
OR->>GS: start
GS-->>OR: ready (address, port)
OR-->>GB: server { address, port }
GB->>GB: persist session(match_id → server, players)
GB-->>P: game_found { match_id, address, port, team }
P->>GS: connect
Step notes
Section titled “Step notes”Step 2: enrich before submitting. IVK Match knows nothing about the player. Everything
the engine needs goes in engine_input: player IDs, MMR, per-host latencies, and the
attributes used by the queue’s attr_filter. Everything the backend wants back goes in
metadata.
Step 4: the backend owns the ticket ID. Generate a UUID before calling. If the call times out, retry with the same ID; it cannot create a duplicate.
Step 5: the ack is not a match. CreateTicket returns when the ticket is durably pooled.
Show the player a queue state, not a match.
Step 8: the outcome is the match. By the time match.created is delivered, the tickets
have already left the pool. That is a commitment: those players are no longer matchmaking.
A backend that cannot honour it has to compensate actively; see
below.
Step 9: host.preferred drives placement. The latency map keys the backend supplied in
engine_input come back as engine_output.host.preferred, with every mutually-acceptable
alternative in host.acceptable. If those keys are the platform’s own region or pool
identifiers, this step is a direct pass-through with no translation layer.
Step 10: provisioning is the backend’s call. The orchestrator API, its authentication, and its retry policy all stay on the platform’s side of the boundary.
Step 13: correlate via match_id. Match.id is a UUID and the natural session key. It
is also the value the game server will later need for backfill requests.
Where the time goes
Section titled “Where the time goes”| Phase | Typical | Driven by |
|---|---|---|
CreateTicket round trip | single-digit ms | Network |
| Waiting in queue | seconds to minutes | Population, wait_time, ruleset strictness |
| Match formed → outcome delivered | tens of ms | Sink; webhook adds one HTTP round trip |
| Server provisioning | seconds | Orchestrator; usually the dominant term after queue wait |
| Notify player | ms | The backend’s own push channel |
Queue wait and server provisioning dominate. tick_rate_secs: 1 means the engine
contributes at most one second of scheduling latency.
Cancellation
Section titled “Cancellation”The player backs out. There is a race here, and you have to handle it.
sequenceDiagram
autonumber
actor P as Player
participant GB as Game Backend
participant IM as IVK Match
P->>GB: cancel_matchmaking(ticket_id)
GB->>IM: CancelTicket(queue_id, ticket_id)
alt Ticket still waiting in the pool
IM-->>GB: ok
GB-->>P: cancelled
else Ticket already committed to a match
IM-->>GB: FAILED_PRECONDITION
Note over GB: The match is already formed.<br/>Its outcome is in flight.
GB-->>P: too late, match found
IM->>GB: outcome: match.created
GB->>GB: proceed with provisioning
end
A failed CancelTicket does not mean the player is still in queue. It usually means
the opposite, so treat FAILED_PRECONDITION as “a match is coming, prepare to honour it”.
Drop the match.created that follows and you strand every other player in that match.
Provisioning failure and compensation
Section titled “Provisioning failure and compensation”The match is real but no server can be had. Those players go back into the pool; do not dump them out to the menu.
sequenceDiagram
autonumber
participant IM as IVK Match
participant GB as Game Backend
participant OR as Orchestrator
actor P as Players
IM->>GB: outcome: match.created { match_id, tickets, host }
GB->>OR: CreateGameServer(host = host.preferred)
OR-->>GB: error, capacity exhausted
loop for each host in host.acceptable
GB->>OR: CreateGameServer(host)
OR-->>GB: error
end
Note over GB: No server available on any acceptable host
GB->>IM: ReactivateTickets(queue_id, ticket_ids)
IM-->>GB: failed_ticket_ids: []
Note over IM: Tickets are back in the pool,<br/>keeping their original created_at
GB-->>P: still searching (no error shown)
Three things make this work:
host.acceptable gives you a fallback list for free. Every host in it is tolerable to
every ticket in the match. Walk it before giving up.
ReactivateTickets restores queue position. Tickets keep their original created_at,
so a player who waited 90 seconds does not go to the back of the line.
terminal_retention_ttl_secs is your compensation window. A resolved ticket can only
be reactivated while it is still retained. Any ticket in failed_ticket_ids was evicted
and needs a fresh CreateTicket. Set pool.tickets.terminal_retention_ttl_secs above the
orchestrator’s worst-case provisioning timeout plus the fallback walk. The default of
300 s is comfortable for most platforms; production configs use 3600 s where provisioning
can be slow.
Search timeout
Section titled “Search timeout”A ticket that never matches expires on its own.
sequenceDiagram
autonumber
actor P as Player
participant GB as Game Backend
participant IM as IVK Match
GB->>IM: CreateTicket(...)
Note over IM: No compatible opponents found within<br/>pool.tickets.expiration_ttl_secs
IM->>IM: cleanup pass evicts the ticket
IM->>GB: outcome: ticket.expired { ticket_ids: [...] }
GB-->>P: search timed out, retry / widen filters?
expiration_ttl_secs is effectively the maximum time a player can sit in queue. Set it a
little above the longest wait the game is willing to display, so the timeout the player
sees is the game’s decision and not a surprise from the matchmaker.
ticket.expired is batched: one outcome can carry many ticket IDs.
Backfill
Section titled “Backfill”A running match has open slots: someone disconnected, or it started under-full.
sequenceDiagram
autonumber
participant GS as Game Server
participant GB as Game Backend
participant IM as IVK Match
actor NP as New Player
Note over GS: A player leaves, 2 slots open
GS->>GB: report_open_slots(match_id, 2)
GB->>IM: CreateBackfillRequest(queue_id, match_id,<br/>ruleset_id, slots_requested=2, engine_input)
IM-->>GB: backfill_id
NP->>GB: queue_matchmaking(...)
GB->>IM: CreateTicket(...)
IM->>IM: backfill stage assigns tickets to the request
IM->>GB: outcome: backfill.created<br/>{ request, assigned_tickets }
GB-->>NP: game_found { address, port }
NP->>GS: connect
Note over GS: Match is full again
GS->>GB: report_full(match_id)
GB->>IM: CancelBackfillRequests(queue_id, [backfill_id])
Worth knowing:
- A new request for the same
match_idreplaces the previous one, so a running server can re-post its current open-slot count on a timer instead of tracking request state. ruleset_idis required whenever the queue has more than one ruleset.Backfill.requestechoes the original request in full, so the outcome is self-describing: no side lookup to work out which match the new players belong to.- Cancel the request when the match fills or ends. Otherwise it sits until
backfill_requests.expiration_ttl_secsand emitsbackfill.expired.
Planned. The backfill API, pool model, and outcome types are all in place, but the engine does not yet fulfil backfill requests: its backfill stage is currently a no-op. A request submitted today is accepted and pooled, and eventually emits
backfill.expiredinstead ofbackfill.created. Work on it is underway. Design against the flow above, but do not gate a launch on it until it ships.
Ticket lifecycle
Section titled “Ticket lifecycle”Every state the backend can observe, and what moves between them.
stateDiagram-v2
[*] --> Active: CreateTicket
Active --> Resolved: match formed → match.created
Active --> Cancelled: CancelTicket
Active --> Expired: expiration_ttl_secs elapsed → ticket.expired
Active --> Removed: queue cleared / retired / params changed → ticket.removed
Resolved --> Active: ReactivateTickets
Resolved --> [*]: terminal_retention_ttl_secs elapsed
Cancelled --> [*]: terminal_retention_ttl_secs elapsed
Expired --> [*]: terminal_retention_ttl_secs elapsed
Removed --> [*]
The edge to remember is Resolved → Active via ReactivateTickets. It only exists while
the ticket is still retained, and it is the only way to undo a match the backend cannot
honour.
Model B: matchmaker-driven provisioning
Section titled “Model B: matchmaker-driven provisioning”Planned. Requires a provisioning integration inside IVK Match.
IVK Match calls the orchestrator itself and returns a fully-resolved match, server address included.
sequenceDiagram
autonumber
actor P as Player
participant GB as Game Backend
participant IM as IVK Match
participant OR as Orchestrator
participant GS as Game Server
P->>GB: queue_matchmaking(...)
GB->>IM: CreateTicket(...)
IM-->>GB: ticket_id
GB-->>P: queued
IM->>IM: match formed
IM->>OR: CreateGameServer(host = preferred, match_id)
OR->>GS: start
GS-->>OR: ready
OR-->>IM: server { address, port }
IM->>GB: outcome: match.created<br/>{ match_id, tickets, teams, server{address,port} }
GB-->>P: game_found { address, port, team }
P->>GS: connect
What this buys
Section titled “What this buys”- A customer with no backend at all can integrate: submit tickets, receive a match that is ready to join.
- One fewer round trip and one fewer failure surface in customer code.
- Provisioning failures are retried and compensated inside IVK Match, which already owns the pool. Reactivating tickets becomes an internal operation instead of a contract the customer has to implement correctly.
- The platform expresses placement policy (capacity, cost, region affinity) once, in the matchmaker’s provisioning config, instead of relying on every customer’s backend to get it right.
What it requires
Section titled “What it requires”- A provisioning driver in IVK Match: an outbound HTTP client for the orchestrator’s API, or a generic webhook contract the platform implements on its side.
- Credential management per tenant, and a place to configure the mapping from host keys to orchestrator placement parameters.
- A rollback path: when provisioning fails on every acceptable host, the match is abandoned and its tickets returned to the pool, with the attempt bounded by a timeout so a slow orchestrator cannot stall the queue.
- An extension to the
Matchmessage carrying the server address, and a decision about whether that is engine-agnostic (onMatch) or engine-specific (insideengine_output). Engine-agnostic is the better shape. - Idempotency against the orchestrator, so a retried provisioning call does not leak servers.
A generic webhook alternative
Section titled “A generic webhook alternative”Rather than IVK Match speaking any particular orchestrator’s API, it can POST a
provisioning request to a platform-supplied URL and await a response:
POST <PROVISION_WEBHOOK_URL>{ "match_id": "…", "queue_id": "acme.quickplay", "ruleset_id": "5v5", "preferred_host": "eu-central", "acceptable_hosts": ["eu-central", "eu-west"], "player_count": 10, "teams": [ { "id": "a", "player_ids": ["…"] }, … ]}→ 200 { "address": "1.2.3.4", "port": 27015, "host": "eu-central" }→ 503 { "error": "no_capacity" } // IVK Match tries the next acceptable hostThis keeps the orchestrator’s API private to the platform, and IVK Match ships one integration instead of one per orchestrator. It is the recommended shape for Model B.
Integration checklist
Section titled “Integration checklist”For a game team wiring up Model A:
- Generate ticket IDs as UUIDs in the backend; retry
CreateTicketwith the same ID on timeout. - Put everything the engine needs in
engine_input; everything you want back inmetadata. - Use host keys in
Player.latenciesthat map directly onto the platform’s placement identifiers. - Consume outcomes idempotently, keyed on
Outcome.idorMatch.id. - Handle
match.createdas a commitment: provision, or compensate withReactivateTickets. - Walk
host.acceptablebefore declaring provisioning failed. - Set
terminal_retention_ttl_secsabove the worst-case provisioning time. - Handle
FAILED_PRECONDITIONonCancelTicketas “a match is coming”. - Retry
UNAVAILABLEwith backoff rather than surfacing it to the player. - Handle
ticket.expiredandticket.removed; both mean the player is out of the pool and needs to be told or re-queued. - Cancel backfill requests when a match fills or ends.