MatchmakingStandalone Matchmaker

Standalone Matchmaker

In the default Colyseus setup, matchmaking and room handling run together in the same process. You can separate them so that a dedicated matchmaker process handles all matchmaking logic, while separate game server processes handle the actual room connections.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Matchmaker Process   β”‚  ← handles join/create/query
β”‚   (no rooms here)       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚  IPC via Redis
      β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”
      β–Ό             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Game Srv  β”‚ β”‚ Game Srv  β”‚  ← rooms run here
β”‚ (rooms)   β”‚ β”‚ (rooms)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

This architecture is useful when you want to:

  • Have a single entry point for matchmaking while distributing room workload across multiple game servers
  • Scale game servers independently from the matchmaking layer
  • Run the matchmaker behind a lightweight HTTP service without a WebSocket transport

Requirements

The matchmaker process and the game server processes must share the same Presence (e.g. Redis) and Driver (e.g. Redis or Postgres) instances. Sharing them lets the processes communicate via IPC and share room cache state.

Game Server Processes

Each game server process runs a full Colyseus server with its rooms defined using defineServer(). These processes handle the actual WebSocket connections and room logic.

app.config.ts
import { defineServer, defineRoom } from "colyseus";
import { RedisPresence } from "@colyseus/redis-presence";
import { RedisDriver } from "@colyseus/redis-driver";
import { WebSocketTransport } from "@colyseus/ws-transport";
import { BattleRoom } from "./rooms/BattleRoom";
 
const gameServer = defineServer({
    transport: new WebSocketTransport(),
    presence: new RedisPresence(),
    driver: new RedisDriver(),
    publicAddress: "game-server-1.example.com:2567",
    rooms: {
        battle: defineRoom(BattleRoom),
    },
});

Each game server must have a unique publicAddress so that the client SDK knows where to establish its WebSocket connection after receiving a seat reservation.

Matchmaker Process

To run a standalone matchmaker, use defineServer() with the isStandaloneMatchMaker option set to true. This option tells Colyseus that the process only handles matchmaking. A standalone matchmaker will not spawn rooms locally, subscribe to IPC room creation requests, or register itself as an available game server process.

app.config.ts
import { defineServer, defineRoom } from "colyseus";
import { RedisPresence } from "@colyseus/redis-presence";
import { RedisDriver } from "@colyseus/redis-driver";
import { BattleRoom } from "./rooms/BattleRoom";
 
const server = defineServer({
    isStandaloneMatchMaker: true,
    presence: new RedisPresence(),
    driver: new RedisDriver(),
    rooms: {
        battle: defineRoom(BattleRoom),
    },
});

When isStandaloneMatchMaker is enabled, the process:

  • Skips IPC subscription: it will not receive or handle remote room creation requests
  • Skips health checks: it does not register itself in the process stats, so room creation never targets it while a healthy game server is registered. Two fallbacks still land a room here: when no game-server stats exist yet, and when a remote room-creation request times out over IPC.
  • Delegates room creation: when a matchmaking request requires a new room, the default selectProcessIdToCreateRoom will select one of the available game server processes
⚠️

The matchmaker process must register the same room types as the game servers. Room classes are needed for filterBy() options and onAuth() validation during matchmaking, even though rooms are not instantiated on this process.

Custom process selection

By default, rooms are created on the game server process with the fewest rooms. You can override this with selectProcessIdToCreateRoom:

app.config.ts
import { defineServer, defineRoom } from "colyseus";
import { RedisPresence } from "@colyseus/redis-presence";
import { RedisDriver } from "@colyseus/redis-driver";
import { BattleRoom } from "./rooms/BattleRoom";
 
const server = defineServer({
    isStandaloneMatchMaker: true,
    presence: new RedisPresence(),
    driver: new RedisDriver(),
    selectProcessIdToCreateRoom: async (roomName, clientOptions) => {
        // Custom logic to select which game server should create the room
    },
    rooms: {
        battle: defineRoom(BattleRoom),
    },
});

How it works

  1. A client sends a matchmaking request (e.g. joinOrCreate) to the matchmaker process
  2. The matchmaker finds or creates a room (room creation is delegated to a game server process via IPC)
  3. The matchmaker returns a seat reservation containing the sessionId, roomId, and the game server’s publicAddress
  4. The client SDK uses the publicAddress to establish a WebSocket connection directly to the game server hosting the room

Clients receive a seat reservation with the publicAddress of the game server that created the room. The client SDK uses this address to connect directly to the game server. Traffic does not flow through the matchmaker process after matchmaking.