Skip to content

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.

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.


version: 1 # required: schema version, always 1
queue_id: acme.quickplay # required: stable identity, dot-separated path allowed
name: 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_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.

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_id must be unique within the queue.
  • engine_id is always nemesis, and must be the same on every ruleset in the queue. Declare it once as default_engine_id and omit it per ruleset.
  • params may 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”.

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.


The minimum that will match anything. Good for a first integration test.

version: 1
queue_id: acme.smoketest
name: Smoke Test
rulesets:
- 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.

The common production shape: one queue, one ruleset.

version: 1
queue_id: acme.quickplay
name: Quick Play
labels:
tenant: acme-games
rulesets:
- 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: 1
queue_id: acme.arena
name: Arena
labels:
tenant: acme-games
default_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 endpoint

D. 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: 1
queue_id: acme.objective
name: Objective Modes
default_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: 5000

Note 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.


FieldRequiredDefaultDescription
versionyesAlways 1.
queue_idyesStable identity. Dot-separated tokens of a-zA-Z0-9_-.
nameyesHuman-readable label.
retirednofalsetrue tears the queue down. See retiring a queue.
labelsno{}String map attached to every queue-scoped metric.
default_engine_idnoEngine for rulesets that omit engine_id.
engine_defaultsno{}Per-engine name / params merged into each matching ruleset.
rulesetsyesAt least one ruleset.
confignoall defaultsSections below.
FieldRequiredDefaultDescription
ruleset_idyesUnique within the queue. Tickets reference it by this value.
engine_idif no default_engine_idnemesis. Must be identical across all rulesets in the queue.
namenofrom engine_defaultsHuman-readable label.
paramsnofrom engine_defaultsEngine parameters, merged over engine_defaults. Full reference: Nemesis.
fairness_weightno1.0Relative priority when rulesets contend for the same ticket. Must be finite and greater than zero.

Controls how long entries live in the pool.

FieldDefaultDescription
cleanup_interval_secs60How often the eviction pass runs. Zero is rejected, and the value may be capped.
tickets.expiration_ttl_secs1800How long an unmatched ticket stays eligible before expiring.
tickets.terminal_retention_ttl_secs300How long a matched/cancelled ticket stays queryable before eviction.
backfill_requests.expiration_ttl_secs1800Same, for backfill requests.
backfill_requests.terminal_retention_ttl_secs300Same, 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.

Controls the matchmaking tick.

FieldDefaultDescription
tick_rate_secs1How often the engine runs. Must be non-zero.
invoke_timeout_secs15Hard cap on one engine invocation. Exceeding it fails the tick and enters backoff.
max_tickets_per_invocation1000Upper bound on tickets handed to the engine per tick. The main lever on per-tick CPU.
rate_window_seconds30Sliding window for the inflow/outflow rates exposed to the engine and to metrics. Must be non-zero.
max_metadata_bytes4096Per-ticket cap on caller-owned metadata.
max_engine_input_bytes32768Per-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.

Controls arbitration when several rulesets want the same ticket.

FieldDefaultDescription
deficit_weight1.0Weight of a ruleset’s service deficit (how underserved it has been).
age_weight1.0Weight of the contested match’s oldest-ticket wait.
age_norm_secs120Wait time at which the age term saturates.
ewma_half_life_secs60Half-life of the demand/service smoothing.
deferral_ticks0Ticks 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.

Where this queue’s formed matches, expirations, and removals are delivered. Full discussion of the trade-offs is in Outcome Delivery.

FieldDefaultDescription
sinkwebhookwebhook or websocket.
webhook.urlRequired when sink: webhook. HTTPS endpoint receiving deliveries.
webhook.secretRequired when sink: webhook. HMAC-SHA256 signing key; the receiver verifies X-IVK-Signature.
webhook.encodingprotobufprotobuf or json.
webhook.timeout_ms5000Per-attempt delivery timeout.
webhook.max_retries8Retry 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: 8

Delivery 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.


ChangeEffect
Valid edit to a live queueApplied in place. The queue keeps serving; no tickets are dropped
Invalid edit to a live queueRejected. The queue keeps running its last known-good configuration
New valid queueStarts serving within seconds
New invalid queueRejected and reported. Other queues are unaffected
retired: trueThe queue is torn down
version: 1
queue_id: acme.quickplay
name: Quick Play
retired: true
rulesets:
- ruleset_id: 5v5
engine_id: nemesis

Retirement 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.


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.