Queue Configuration
A queue groups players who want the same kind of match, and it is the unit of configuration: one YAML document describes one queue completely. Team shape, skill tolerance, latency budgets, attribute rules, and where outcomes are delivered all live in the same place.
Where configuration lives
Section titled “Where configuration lives”You author a queue as YAML and manage it in the portal. There is no registration API to call, no database to update, and no deploy on your side.
Edits apply to the running service within seconds of being saved. A queue keeps serving across a valid edit and no waiting players are dropped. An edit that fails validation is rejected and the queue keeps running its last known-good configuration, so a bad edit cannot take a live queue down. The flip side: if you changed something and nothing happened, check for a validation error before concluding that reload is broken.
Anatomy of a queue
Section titled “Anatomy of a queue”version: 1 # required: schema version, always 1queue_id: acme.quickplay # required: stable identity, dot-separated path allowedname: Quick Play # required: human-readable label
labels: # optional: attached to every metric for this queue tenant: acme-games game: robot-arena
default_engine_id: nemesis # optional: engine for rulesets that don't name one
engine_defaults: # optional: params shared by all rulesets on an engine nemesis: params: wait_time: 90 max_latency_ms: 200
rulesets: # required: at least one - ruleset_id: 5v5 # unique within the queue fairness_weight: 3 # optional (default 1.0): relative priority in conflicts params: # engine params; merged over engine_defaults team_size: { start: 5, end: 5 } team_count: { start: 2, end: 2 }
config: # optional: all sections have defaults pool: { ... } invocation: { ... } fairness: { ... } outcomes: { ... }Queue IDs
Section titled “Queue IDs”queue_id may be a dot-separated path, such as acme.quickplay or acme.ranked.solo,
where each token uses a-zA-Z0-9_-. Namespacing this way is recommended: it groups a
customer’s queues in the dashboard and lets outcome consumers subscribe to one namespace
rather than filtering client-side.
Rulesets
Section titled “Rulesets”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 chooses which rulesets it competes in via its ruleset_ids field.
Constraints:
ruleset_idmust be unique within the queue.engine_idis alwaysnemesis, and must be the same on every ruleset in the queue. Declare it once asdefault_engine_idand omit it per ruleset.paramsmay differ freely between rulesets. Full reference: Nemesis.
When the same ticket is a candidate in matches from more than one ruleset in the same
tick, the runtime arbitrates using fairness_weight and the config.fairness settings
below. A higher fairness_weight makes a ruleset win conflicts more often. This is how you
say “5v5 is our headline mode, FFA is a side mode”.
engine_defaults and params merging
Section titled “engine_defaults and params merging”Repeating twenty engine parameters across five rulesets will bite you eventually. Declare
them once under engine_defaults.<engine_id>.params and override per ruleset. Merging
follows JSON Merge Patch semantics: objects merge recursively, while scalars and arrays
are replaced wholesale. A ruleset that sets attr_filter replaces the default
attr_filter entirely instead of appending to it. Setting a param to null in a ruleset
deletes the inherited default.
Unknown fields are rejected at parse time, so a typo fails the config instead of doing nothing.
Worked examples
Section titled “Worked examples”A. Simplest possible queue: 5v5
Section titled “A. Simplest possible queue: 5v5”The minimum that will match anything. Good for a first integration test.
version: 1queue_id: acme.smoketestname: Smoke Testrulesets: - ruleset_id: default engine_id: nemesis params: team_size: { start: 5, end: 5 } team_count: { start: 2, end: 2 }Everything else defaults. Skill matching is on (mmr_weight: 1.0), latency scoring is
off (latency_weight: 0.0) though the hard 200 ms host filter still applies, and no
attribute filtering happens because attr_filter defaults to empty. See
Nemesis for what that means in practice.
B. Skill- and latency-aware quick play
Section titled “B. Skill- and latency-aware quick play”The common production shape: one queue, one ruleset.
version: 1queue_id: acme.quickplayname: Quick Playlabels: tenant: acme-gamesrulesets: - ruleset_id: 5v5 engine_id: nemesis params: team_size: { start: 5, end: 5 } team_count: { start: 2, end: 2 }
# How long the engine is willing to keep growing a match, in ticks. wait_time: 120
# Relative importance of connection quality vs. fair skill. # latency_weight defaults to 0.0; latency is only *scored* if you set it. latency_weight: 0.5 mmr_weight: 0.5
# Worst-case acceptable skill spread, in YOUR mmr units. # This must match the scale you send in engine_input. mmr_normalization_ref: 0.2
# Hard latency gate: a host is only acceptable below this RTT. max_latency_ms: 200 default_latency_ms: 150 # assumed when a player has no measurement
# Only group players who agree on these ticket attributes. attr_filter: - playlist # overlap: share at least one value - name: platform mode: containment # crossplay opt-in must be mutual
config: pool: tickets: expiration_ttl_secs: 1800 # 30 min before a waiting ticket ages out terminal_retention_ttl_secs: 300 # matched tickets stay queryable 5 min invocation: tick_rate_secs: 1 max_tickets_per_invocation: 5000 outcomes: sink: webhook # where formed matches are delivered webhook: url: https://backend.acme.example/ivk/outcomes secret: ${ACME_OUTCOME_SECRET}C. Multi-mode queue with a shared engine baseline
Section titled “C. Multi-mode queue with a shared engine baseline”One queue, three modes, engine tuning declared once. Taken from a live configuration.
version: 1queue_id: acme.arenaname: Arenalabels: tenant: acme-gamesdefault_engine_id: nemesis
engine_defaults: nemesis: params: attr_filter: - playlist - name: platform mode: containment wait_time: 150
latency_normalization_ref_ms: { start: 15, end: 90 } latency_weight: 1.0 mmr_weight: 1.0 mmr_normalization_ref: 0.2
latency_spread_weight: 1.0 latency_abs_weight: 4.0
default_latency_ms: 100 max_latency_ms: 200
# Party handling: parties get a small MMR bonus per extra member, and # members are pulled toward the party's strongest player. party_synergy_bonus: 0.05 party_blend_to_max: 0.25
rulesets: - ruleset_id: 4v4 fairness_weight: 9 # headline mode; wins most conflicts params: team_size: { start: 4, end: 4 } team_count: { start: 2, end: 2 } - ruleset_id: 3v3 fairness_weight: 5 params: team_size: { start: 3, end: 3 } team_count: { start: 2, end: 2 } - ruleset_id: ffa_8p fairness_weight: 3 # side mode; yields to the others params: team_size: { start: 8, end: 8 } team_count: { start: 1, end: 1 } # one team = free-for-all
config: fairness: deferral_ticks: 10 # hold a contested match 10 ticks so a slower # ruleset can field a competitor deficit_weight: 1.0 age_weight: 1.0 age_norm_secs: 120 pool: tickets: expiration_ttl_secs: 1800 invocation: tick_rate_secs: 1 invoke_timeout_secs: 15 max_tickets_per_invocation: 5000 outcomes: sink: websocket # this backend streams outcomes instead of # exposing an inbound HTTPS endpointD. Per-mode playlist gating and per-origin latency caps
Section titled “D. Per-mode playlist gating and per-origin latency caps”Different playlists routed to different team sizes within one queue, with per-origin latency budgets.
version: 1queue_id: acme.objectivename: Objective Modesdefault_engine_id: nemesis
engine_defaults: nemesis: params: attr_filter: - playlist - name: platform mode: containment wait_time: 90
latency_normalization_ref_ms: { start: 55, end: 200 } latency_weight: 0.4 mmr_weight: 0.6
default_latency_ms: 200 max_latency_ms: 200
# Per-origin hard caps override max_latency_ms for tickets whose origin # host is listed. Keys are host keys from Player.latencies. max_latency_by_origin: us-east: 80 us-west: 80 eu-central: 80 oce-sydney: 125 sea-singapore: 125 sa-saopaulo: 150
# Widen each ticket's acceptable-host set until it covers at least this # fraction of the pool, so a match is never pinned to one fragile host. min_host_coverage_frac: 0.10
rulesets: - ruleset_id: exact_6v6 fairness_weight: 6 params: attr_filter: - name: playlist allowed_values: ["TDM", "Upload", "Intel", "Capture_The_Flag"] - name: platform mode: containment team_size: { start: 6, end: 6 } team_count: { start: 2, end: 2 } ticket_pool_size_target: 120
- ruleset_id: exact_4v4 fairness_weight: 1 params: attr_filter: - name: playlist allowed_values: ["Duel_Arena"] - name: platform mode: containment team_size: { start: 4, end: 4 } team_count: { start: 2, end: 2 } ticket_pool_size_target: 100
config: fairness: deferral_ticks: 5 pool: cleanup_interval_secs: 60 tickets: expiration_ttl_secs: 1800 terminal_retention_ttl_secs: 3600 invocation: tick_rate_secs: 1 invoke_timeout_secs: 15 max_tickets_per_invocation: 5000Note that the ruleset-level attr_filter replaces the one in engine_defaults
(arrays are not merged), which is why platform is repeated in each ruleset.
Configuration reference
Section titled “Configuration reference”Top level
Section titled “Top level”| Field | Required | Default | Description |
|---|---|---|---|
version | yes | — | Always 1. |
queue_id | yes | — | Stable identity. Dot-separated tokens of a-zA-Z0-9_-. |
name | yes | — | Human-readable label. |
retired | no | false | true tears the queue down. See retiring a queue. |
labels | no | {} | String map attached to every queue-scoped metric. |
default_engine_id | no | — | Engine for rulesets that omit engine_id. |
engine_defaults | no | {} | Per-engine name / params merged into each matching ruleset. |
rulesets | yes | — | At least one ruleset. |
config | no | all defaults | Sections below. |
rulesets[]
Section titled “rulesets[]”| Field | Required | Default | Description |
|---|---|---|---|
ruleset_id | yes | — | Unique within the queue. Tickets reference it by this value. |
engine_id | if no default_engine_id | — | nemesis. Must be identical across all rulesets in the queue. |
name | no | from engine_defaults | Human-readable label. |
params | no | from engine_defaults | Engine parameters, merged over engine_defaults. Full reference: Nemesis. |
fairness_weight | no | 1.0 | Relative priority when rulesets contend for the same ticket. Must be finite and greater than zero. |
config.pool
Section titled “config.pool”Controls how long entries live in the pool.
| Field | Default | Description |
|---|---|---|
cleanup_interval_secs | 60 | How often the eviction pass runs. Zero is rejected, and the value may be capped. |
tickets.expiration_ttl_secs | 1800 | How long an unmatched ticket stays eligible before expiring. |
tickets.terminal_retention_ttl_secs | 300 | How long a matched/cancelled ticket stays queryable before eviction. |
backfill_requests.expiration_ttl_secs | 1800 | Same, for backfill requests. |
backfill_requests.terminal_retention_ttl_secs | 300 | Same, for backfill requests. |
expiration_ttl_secs is the practical ceiling on how long a player can sit in queue.
Set it to slightly more than the longest wait the game is willing to show a player; when
it fires, a ticket.expired outcome is emitted so the backend can surface a timeout.
config.invocation
Section titled “config.invocation”Controls the matchmaking tick.
| Field | Default | Description |
|---|---|---|
tick_rate_secs | 1 | How often the engine runs. Must be non-zero. |
invoke_timeout_secs | 15 | Hard cap on one engine invocation. Exceeding it fails the tick and enters backoff. |
max_tickets_per_invocation | 1000 | Upper bound on tickets handed to the engine per tick. The main lever on per-tick CPU. |
rate_window_seconds | 30 | Sliding window for the inflow/outflow rates exposed to the engine and to metrics. Must be non-zero. |
max_metadata_bytes | 4096 | Per-ticket cap on caller-owned metadata. |
max_engine_input_bytes | 32768 | Per-ticket cap on engine_input. |
The two size caps are enforced at the API boundary before any parsing; violations are
rejected with INVALID_ARGUMENT. They bound worst-case memory directly, so lower them if
the game’s payloads are small.
config.fairness
Section titled “config.fairness”Controls arbitration when several rulesets want the same ticket.
| Field | Default | Description |
|---|---|---|
deficit_weight | 1.0 | Weight of a ruleset’s service deficit (how underserved it has been). |
age_weight | 1.0 | Weight of the contested match’s oldest-ticket wait. |
age_norm_secs | 120 | Wait time at which the age term saturates. |
ewma_half_life_secs | 60 | Half-life of the demand/service smoothing. |
deferral_ticks | 0 | Ticks a contested multi-ruleset match is held before commit, so a slower ruleset can field a competitor. 0 = same-tick arbitration only. Max 60. |
In a single-ruleset queue you can ignore this section entirely. In a multi-mode queue,
deferral_ticks in the range 5 to 10 noticeably improves how evenly the modes are
served, at the cost of that many ticks of extra latency on contested matches.
config.outcomes
Section titled “config.outcomes”Where this queue’s formed matches, expirations, and removals are delivered. Full discussion of the trade-offs is in Outcome Delivery.
| Field | Default | Description |
|---|---|---|
sink | webhook | webhook or websocket. |
webhook.url | — | Required when sink: webhook. HTTPS endpoint receiving deliveries. |
webhook.secret | — | Required when sink: webhook. HMAC-SHA256 signing key; the receiver verifies X-IVK-Signature. |
webhook.encoding | protobuf | protobuf or json. |
webhook.timeout_ms | 5000 | Per-attempt delivery timeout. |
webhook.max_retries | 8 | Retry budget before a delivery is parked and alerted on. |
config: outcomes: sink: webhook webhook: url: https://backend.example.com/ivk/outcomes secret: ${ACME_OUTCOME_SECRET} encoding: protobuf timeout_ms: 5000 max_retries: 8Delivery is configured per queue rather than per game. Two of your queues can deliver to different endpoints, and you can repoint or re-secure delivery with a configuration edit instead of a deploy.
Store the signing secret in the portal rather than inline, and reference it as above. That keeps the queue YAML safe to hold in your own source control.
Applying changes and retiring
Section titled “Applying changes and retiring”| Change | Effect |
|---|---|
| Valid edit to a live queue | Applied in place. The queue keeps serving; no tickets are dropped |
| Invalid edit to a live queue | Rejected. The queue keeps running its last known-good configuration |
| New valid queue | Starts serving within seconds |
| New invalid queue | Rejected and reported. Other queues are unaffected |
retired: true | The queue is torn down |
Retiring a queue
Section titled “Retiring a queue”version: 1queue_id: acme.quickplayname: Quick Playretired: truerulesets: - ruleset_id: 5v5 engine_id: nemesisRetirement is not a drain. Pooled tickets are cleared, not matched to completion: every
waiting player is removed and a ticket.removed outcome is emitted with reason
QUEUE_RETIRED. New submissions are rejected with FAILED_PRECONDITION and the metadata
header x-ivk-reason: QUEUE_RETIRING.
If retiring a queue must not strand players, stop routing new tickets to it in your backend
first, wait for the pool to empty, then set retired: true.
Changing a ruleset’s engine params in place can also strand pooled tickets. If a ticket
no longer satisfies the new params (after an attr_filter change, say), the revalidation
sweep removes it with reason RULESET_PARAMS_CHANGED. The game backend should treat
ticket.removed as an ordinary event and re-queue the player.
Troubleshooting
Section titled “Troubleshooting”A queue is serving stale configuration. The edit almost certainly failed validation, which leaves the previous configuration running. Check for a reported validation error naming the queue.
Tickets rejected for an unknown ruleset. The ticket’s ruleset_ids contains a value not
declared in the queue’s rulesets. A typo, a rename the submitting code did not follow, or the
ticket is going to the wrong queue.
Backfill rejected in a multi-ruleset queue. ruleset_id is required on backfill requests
when a queue has more than one ruleset; there is no sensible default. It may be omitted only in
single-ruleset queues.
Players dropped out of a queue after a configuration change. Changing engine params can
strand pooled tickets that no longer satisfy them, and they are removed with reason
RULESET_PARAMS_CHANGED. This is expected: treat ticket.removed as an ordinary event and
re-queue the player.
Duplicate queue_id. Two queues cannot share an ID. The second is rejected and reported.