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.
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.
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
selectProcessIdToCreateRoomwill 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:
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
- A client sends a matchmaking request (e.g.
joinOrCreate) to the matchmaker process - The matchmaker finds or creates a room (room creation is delegated to a game server process via IPC)
- The matchmaker returns a seat reservation containing the
sessionId,roomId, and the game serverβspublicAddress - The client SDK uses the
publicAddressto 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.