Rooms
The Room class is the core building block of Colyseus. Each room instance holds one group of clients, who interact through shared state and messages.
Why Rooms?
- Isolation - Players in Room A don’t see or interact with players in Room B
- Encapsulation - Each room contains its own state, logic, and connected clients
- Scalability - Rooms are created on demand and (optionally) disposed when empty
- Flexibility - One room class can spawn many instances (e.g., multiple matches of the same game type)
Defining a Room
You can define a Room using a class that extends Room.
import { Room, Client } from "colyseus";
import { MyState } from "./MyState";
export class MyRoom extends Room {
state = new MyState();
onJoin(client: Client, options: any) {
client.send("welcome", "Welcome to the room!");
}
}Room State
Set the synchronizable room state. See State Synchronization and Schema for more details.
import { Room } from "colyseus";
import { MyState } from "./MyState";
export class MyRoom extends Room {
state = new MyState();
}The room’s state is mutable. You should not reassign the state object, but rather mutate it directly when updating the state.
Message Handling
Rooms have these methods available.
On Messages
Register handlers to process messages sent by the frontend.
- The
typeargument can be eitherstringornumber. - You can only define a single handler per message type.
Handler for specific type of message
messages = {
"action": (client, payload) => {
console.log(client.sessionId, "sent 'action' message: ", payload);
}
}Use room.send(type, payload) from the client SDK to send messages to the server.
Fallback for all messages
You can register a single handler as a fallback to handle other types of messages.
messages = {
"action": (client, payload) => {
//
// Triggers when 'action' message is sent.
//
},
"*": (client, type, payload) => {
//
// Triggers when any other type of message is sent,
// excluding "action", which has its own specific handler defined above.
//
console.log(client.sessionId, "sent", type, payload);
}
}Registering handlers at runtime
The declarative messages map covers most cases. You may also register a handler imperatively with this.onMessage(). It returns a function that removes the handler when called.
onCreate() {
const unbind = this.onMessage("action", (client, payload) => {
// ...
});
// stop handling "action" messages
unbind();
}Use this.onMessageBytes() to receive raw Uint8Array messages sent via room.sendBytes() from the client SDK, skipping the default MsgPack decoding.
onCreate() {
this.onMessageBytes("raw-input", (client, bytes) => {
// bytes is a Uint8Array: decode it yourself
});
}Message input validation
You may provide a validation schema using the validate() helper with a Zod schema. If validation fails, the client is disconnected with close code 4002 (WITH_ERROR). For a request, the request settles as an error instead.
The validated and typed data will be passed as payload on the message handler.
import { Room, validate } from "colyseus";
import { z } from "zod";
// ...
messages = {
"action": validate(z.object({
x: z.number(),
y: z.number()
}), (client, payload) => {
//
// payload.x and payload.y are guaranteed to be numbers here.
//
console.log({ x: payload.x, y: payload.y });
})
}Responding to a message (request/response)
A message handler can return a value to answer a client that’s waiting for a reply via room.request() or room.send(type, payload, callback). The same handlers serve both plain (fire-and-forget) sends and requests. The return value is sent back only when the client awaited a reply, and ignored otherwise.
messages = {
// Return a value (sync or async): it becomes the client's response.
"get-profile": async (client, { userId }) => {
return await db.profiles.findById(userId);
},
// Throwing (or rejecting) settles the client's request as an error.
"buy-item": (client, { itemId }) => {
if (!this.canAfford(client, itemId)) {
throw new Error("not enough coins");
}
return { ok: true };
},
// The 3rd argument (ctx) allows deliberate rejections with a typed reason.
"join-team": (client, { team }, ctx) => {
if (this.isTeamFull(team)) {
return ctx.reject({ code: "TEAM_FULL", team });
}
return { ok: true };
},
}- The handler’s return value is awaited before being sent, so returning a
Promiseworks. - If the handler throws or its
Promiserejects, the client’s request rejects with that error instead of timing out. - Every handler also receives a third
ctxargument. Callctx.reject(reason)to settle the request as rejected: a deliberate business-logic “no”, distinct from an error. On the client, the rejection arrives as anErrorwithname: "rejected"and the rawreasonon its.reasonproperty. See Request/Response on the SDK. ctx.resolve(value)answers the request explicitly. Use it when the handler must keep working after replying. A plainreturnis equivalent otherwise.- Only the handler registered for the message’s specific type can answer a request. The
"*"fallback is not eligible, and a request for a type with no handler rejects with ano_handlererror.
Lifecycle Events
Rooms expose a hook for every stage a client passes through: onCreate,
onAuth, onJoin, onDrop, onReconnect, onLeave, and onDispose, plus
the devMode and graceful-shutdown hooks. See
Room Lifecycle Events for the full reference.
Room Configuration
Game Loop
Optional: Set a game loop that can change the state of the game. Default interval: 16.6ms (60fps).
Use setTimestep(callback, delay?) for a variable-step loop. The callback receives the measured wall-clock deltaTime:
onCreate () {
this.setTimestep((deltaTime) => this.update(deltaTime));
}
update (deltaTime) {
// implement your physics or world updates here!
// this is a good place to update the room state
}setSimulationInterval() is the previous name for setTimestep() and still works (it forwards). For client-predicted games, use the fixed-step loop setFixedTimestep(step, tickRate) instead. It advances by a constant dt and advertises the rate to predicting clients, which a variable step can’t do deterministically. See Server Input & Fixed Timestep.
Visibility & Access
Three flags (locked, private, and unlisted) control who can find and
join a room, alongside lock(), unlock(), and setMatchmaking(). See
Room Visibility & Access for the full reference.
Configuration Properties
| Property | Default | Description |
|---|---|---|
maxClients | Infinity | Maximum number of clients allowed. Room is auto-locked when full. |
patchRate | 50 | Frequency to send state updates to clients, in milliseconds (20fps). |
autoDispose | true | Automatically dispose the room when last client disconnects. |
maxMessagesPerSecond | Infinity | Maximum messages a client can send per second. Exceeding disconnects the client. |
seatReservationTimeout | 15 | Seconds to wait for a client to effectively join after reserving a seat. |
locked | (read-only) | Whether the room is currently locked (via maxClients or lock()). |
Communication
Broadcast Message
Send a message to all connected clients.
Available options are:
except: aClient, or array ofClientinstances not to send the message toafterNextPatch: waits until next patch to broadcast the message
Broadcasting a message to all clients:
messages = {
"action": (client, payload) => {
// broadcast a message to all clients
this.broadcast("action-taken", "an action has been taken!");
}
}The client will receive the message in the onMessage() callback.
Broadcast Message (in bytes)
Send a raw byte array to all connected clients, skipping the default MsgPack encoding. The counterpart of client.sendBytes() for broadcasts.
this.broadcastBytes("raw-update", new Uint8Array([ 1, 2, 3 ]), { except: client });Has Reached Max Clients
Returns whether the sum of connected clients and reserved seats exceeds the maximum number of clients.
onCreate(options) {
if (this.hasReachedMaxClients()) {
console.log("Room is full!");
}
}Has Reserved Seat
Returns whether a seat reservation exists for the given sessionId: reserved but not yet consumed, or held for a reconnection. Pass the client’s reconnectionToken as the second argument to check a reconnection reservation specifically.
if (this.hasReservedSeat(sessionId)) {
// a client with this sessionId is expected to join
}Disconnect
Disconnect all clients, then dispose of the room. Returns a Promise that resolves when every client has been disconnected.
The optional closeCode is delivered to each client’s onLeave listener. The default is CONSENTED (4000); custom application codes use 4011–4999. See the close-code table.
// disconnect all clients, then dispose of the room
await this.disconnect();
// same, with a custom close code
await this.disconnect(4020);Broadcast Patch
You may not need this! - This method is called automatically by the framework.
This method will check whether mutations have occurred in the state, and broadcast them to all connected clients.
If you’d like to have control over when to broadcast patches, you can do this by disabling the default patch interval:
// disable automatic patches
patchRate = null;
onCreate() {
this.clock.setInterval(() => {
// only broadcast patches if your custom conditions are met.
if (yourCondition) {
this.broadcastPatch();
}
}, 2000);
}Reconnection
allowReconnection() lets a dropped client return to the room within a time
window, or under manual control. See Reconnection for
the full server-side and client-side reference.
Room Properties
roomId
The unique identifier of the room. By default, a random 9-character-long string is assigned as the Room ID.
You may customize the Room ID during onCreate() by setting the this.roomId property. See Recipes » Customize Room ID
roomName
The name of the room you provided in the rooms configuration of defineServer().
state
The synchronized state of the room.
metadata
The room’s matchmaking metadata. This data is used to filter rooms during matchmaking queries.
// Get metadata
console.log(this.metadata.difficulty);
// Set metadata during onCreate() only
onCreate(options) {
this.metadata = { difficulty: "hard", rating: 1500 };
}The metadata setter can only be used during onCreate(). To update metadata after the room is created, use setMetadata() or setMatchmaking() instead.
clients
The array of connected clients. See Client Instance.
Sending a message to a specific client
// ...
this.clients.forEach((client) => {
if (client.userData.team === "red") {
client.send("hello", "world");
}
});
// ...Getting a client by sessionId.
// ...
const client = this.clients.get("UEsBFUBhK");
// ...clock
The clock provides timing controls that automatically clean up when the room is disposed, preventing memory leaks. Use it instead of setTimeout and setInterval.
// ...
onCreate() {
this.clock.setTimeout(() => {
console.log("This message will be printed after 5 seconds");
}, 5000);
this.clock.setInterval(() => {
console.log("Current time:", this.clock.currentTime);
}, 1000);
}
// ...See Timing Events.
presence
The presence is used as a shared in-memory database for your cluster, and for pub/sub operations between rooms.
// ...
onCreate() {
// publish an event to all rooms listening to "event-from-another-room"
this.presence.publish("event-name-from-another-room", { hello: "world" });
// subscribe to events from another room
this.presence.subscribe("event-name-from-another-room", (payload) => {
console.log("Received event from another room!", payload);
});
// set arbitrary value to the presence
this.presence.set("arbitrary-key", "value");
}
// ...See Presence API.
Client Instance
The client instance from the backend is responsible for the transport layer between the server and the client. Do not confuse it with the Client from the frontend SDK, as they have completely different purposes.
You operate on client instances from this.clients, Room#onJoin(), Room#onLeave() and Room#onMessage().
Properties
sessionId
Unique identifier of the client connection.
// ...
onJoin(client, options) {
console.log(client.sessionId);
}
// ...In the frontend, you can find the sessionId in the room instance.
userData
The client.userData can be used to store player-specific data easily accessible via the client instance. This property is meant for convenience.
// ...
onJoin(client, options) {
client.userData = { team: (this.clients.length % 2 === 0) ? "red" : "blue" };
}
onLeave(client) {
console.log(client.userData.team);
}
// ...auth
The client.auth property holds the data returned by the onAuth() method.
onAuth(client, options, context) {
return { userId: "123" };
}
onJoin(client, options) {
console.log(client.auth.userId);
}See Authentication for more details.
view
The client.view property is used for state filtering - allowing you to control which parts of the state a client can see.
import { StateView } from "@colyseus/schema";
onJoin(client, options) {
// Set the client's view to filter portions of the state they receive
client.view = new StateView();
}See State Synchronization → State View for more details.
reconnectionToken
The unique token used for reconnection. This token is regenerated each time the client connects.
onJoin(client, options) {
console.log(client.reconnectionToken);
}Methods
Send Message
Send a type of message to the client. Messages are encoded with MsgPack and can hold any JSON-serializable data structure.
The type can be either a string or a number.
//
// sending message of string type ("powerup")
//
client.send("powerup", { kind: "ammo" });
//
// sending message of number type (1)
//
client.send(1, { kind: "ammo"});Send Message (in bytes)
Send a raw byte array message to the client.
The type can be either a string or a number.
This method is useful if you’d like to manually encode a message, rather than the default encoding (MsgPack).
//
// sending message of string type ("powerup")
//
client.sendBytes("powerup", new Uint8Array([ 172, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33 ]));
//
// sending message of number type (1)
//
client.sendBytes(1, new Uint8Array([ 172, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33 ]));Leave Room
Force disconnection of the client with the room. You may send a custom code when closing the connection, with values between 4011 and 4999. Codes 4000–4010 are reserved by the framework. See the table of WebSocket close codes below.
// disconnect this client with the default code (4000, CONSENTED)
client.leave();
// disconnect this client with a custom code
client.leave(4020);This call will trigger the room.onLeave event on the frontend.
Table of WebSocket close codes
| Close code (uint16) | Codename | Internal | Customizable | Description |
|---|---|---|---|---|
0 - 999 | Yes | No | Unused | |
1000 | NORMAL_CLOSURE | No | No | Successful operation / regular socket shutdown |
1001 | GOING_AWAY | No | No | Client is leaving (browser tab closing) |
1002 | Protocol error | Yes | No | Endpoint received a malformed frame |
1003 | Unsupported frame | Yes | No | Endpoint received an unsupported frame (e.g. binary-only endpoint received text frame) |
1004 | Yes | No | Reserved | |
1005 | NO_STATUS_RECEIVED | Yes | No | Expected close status, received none |
1006 | ABNORMAL_CLOSURE | Yes | No | No close code frame has been received |
1007 | Unsupported payload | Yes | No | Endpoint received inconsistent message (e.g. malformed UTF-8) |
1008 | Policy violation | No | No | Generic code used for situations other than 1003 and 1009 |
1009 | Frame too large | No | No | Endpoint won’t process large frame |
1010 | Mandatory extension | No | No | Client wanted an extension which server did not negotiate |
1011 | Server error | No | No | Internal server error while operating |
1012 | Service restart | No | No | Server/service is restarting |
1013 | Try again later | No | No | Temporary server condition forced blocking client’s request |
1014 | Bad gateway | No | No | Server acting as gateway received an invalid response |
1015 | TLS handshake fail | Yes | No | Transport Layer Security handshake failure |
1016 - 1999 | Yes | No | Reserved for future use by the WebSocket standard. | |
2000 - 2999 | Yes | Yes | Reserved for use by WebSocket extensions | |
3000 - 3999 | No | Yes | Available for use by libraries and frameworks. May not be used by applications. Available for registration at the IANA via first-come, first-serve. | |
4000 | CONSENTED | No | No | Client left intentionally (room.leave()) |
4001 | SERVER_SHUTDOWN | No | No | Server graceful shutdown (production) |
4002 | WITH_ERROR | No | No | Closed due to an error |
4003 | FAILED_TO_RECONNECT | No | No | All reconnection attempts failed, or the server denied/failed the reconnection |
4010 | MAY_TRY_RECONNECT | No | No | Server shutdown in dev mode (allows reconnect) |
4011 - 4999 | No | Yes | Available for applications |
Built-in Rooms
Colyseus ships pre-built room types you can use directly or extend. Lobby and Queue are documented under Matchmaking, since that’s the job they do:
A live listing of available rooms, the backend for a room browser UI.
A matchmaking queue that groups players over time and spawns a match room when a group is ready.
A lightweight relay that broadcasts client messages to everyone else, with no authoritative state.
Next Steps
- State Synchronization - Learn how to define and synchronize room state
- Server Configuration - Configure your Colyseus server
- Timing Events - Schedule delayed and recurring events