Quick match

irtio’s join story is share-a-code: someone creates a room, the code travels out of band, friends join it. That covers party games and second screens and does not cover the quick-play button every arena and versus game wants.

Quick match is that button. It is a queue on the control plane that answers with a room code:

import { matchRoom } from '@irtio/client';
import { schema } from './irtio/schema';

const room = await matchRoom(schema);   // waits for an opponent, then joins

Two players call that. Neither supplies a room code. Both end up in the same room.

What happens

  1. Each client posts to the queue and the request stays open while they wait.
  2. When enough players are waiting, the platform mints one fresh room code and answers everyone in the party with it at the same moment.
  3. Each client joins that code with the ordinary join path.

Step 3 is the important one. There is no special “matched room”: the room is created by whoever joins first, exactly as it would be if a friend had sent the code in a chat message. Capacity, placement, hibernation and everything else behave identically. Your room code cannot tell — and does not need to tell — that a matchmaker was involved.

Waiting, and not matching

The call takes as long as the queue takes. If nobody else shows up before the deadline it throws E_NO_MATCH with how many players were waiting, and you decide what happens next:

try {
  const room = await matchRoom(schema, { timeoutMs: 20_000 });
} catch (err) {
  if (err.code === 'E_NO_MATCH') {
    // Nobody around. Offer a room code, a bot, or a "try again" button.
  }
}

It will not quietly put a player alone in a room and call that a match. A matchmaker that degrades silently is one nobody can debug, and a player sitting in an empty arena wondering where their opponent is has been lied to.

If you want to render your own waiting state, findMatch is the queue call on its own — it resolves with the room code and leaves the joining to you. GET /match/status?project=<id> reads how many are waiting without queueing anyone, which is what “2 of 4 waiting” is made of.

Queues

Every project has one queue without configuring anything: default, a party of two. That is why matchRoom(schema) works against a brand new project.

Named queues are per-project configuration:

curl -X PATCH https://irt.io/v1/projects/<id> 
  -H 'content-type: application/json' 
  -d '{"name":"my game","queues":{"1v1":{"size":2},"ffa":{"size":8}}}'
const room = await matchRoom(schema, { queue: 'ffa' });

Once a project declares any queue, default stops being implicit — a project that wants one says so. Asking for a queue that is not configured is an error rather than a queue of one that never fills.

Identity, and why you want it

const room = await matchRoom(schema, { queue: '1v1', identity: true });

With an identity, the queue holds one ticket per player: queueing twice retires the earlier request instead of matching a player against themselves. Without one, the queue cannot tell two tabs apart from two people, and all that stands between a queue and one bored person filling it is the per-address rate limit.

Quick match works without identities so you can try it in an afternoon. Ship it with them.

What this is not

  • Not skill matching. Strict first-come, first-served within a queue. No rating, no bands.
  • Not backfill. Every match is a fresh room. A game already in progress with a free slot will not be filled by the queue.
  • Not cross-region. One queue, one region — the region the project is placed in.
  • Not a party system. Players queue individually; there is no way to enter as a pre-formed group.

Each of those is a real feature and none of them is here yet. What is here is the one that the other four are variations of.

Limits

POST /match is a public endpoint, so it is rate limited per address and per project. A ticket dies with its connection: close the tab and the entry leaves the queue immediately, so a queue cannot fill with players who will never arrive. The numbers are on the limits page.