Rooms

Rooms

The Room class is the core building block of Colyseus. Each room instance represents an isolated game session where clients can 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.

MyRoom.ts
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!");
    }
}

Lifecycle Events

The room’s lifecycle events are called automatically. async/await is supported in all of them.

MyRoom.ts
import { Room, Client, AuthContext } from "colyseus";
 
export class MyRoom extends Room {
    // (optional) Validate client auth token before joining/creating the room
    onAuth (client: Client, options: any, context: AuthContext) { }
 
    // When room is initialized
    onCreate (options: any) { }
 
    // When the client successfully joins the room
    onJoin (client: Client, options: any, auth: any) { }
 
    // When a client disconnects unexpectedly (use allowReconnection here)
    onDrop (client: Client, code?: number) { }
 
    // When a client successfully reconnects
    onReconnect (client: Client) { }
 
    // When a client effectively leaves the room
    onLeave (client: Client, code?: number) { }
 
    // Cleanup callback, called after there are no more clients in the room. (see `autoDispose`)
    onDispose () { }
}

On Create

Called once, when the room is created by the matchmaker. options is the merged values specified on Server#define() with the options provided from the SDK at .joinOrCreate() or .join().

MyRoom.ts
onCreate (options) {
    /**
     * This is a good place to initialize your room state.
     */
}

The server may overwrite options during .define() for authority over client-provided options:

app.config.ts
// (backend)
const server = defineServer({
  rooms: {
    my_room: defineRoom(MyRoom, { map: "cs_assault" })
  }
})

On this example, the map option is "cs_assault" during onCreate(), and "de_dust2" during onJoin().


On Auth

The onAuth method is called before onJoin, and it is responsible for validating the client’s request to join a room.

MyRoom.ts
async onAuth (client, options, context) {
    /**
     * This is a good place to validate the client's auth token.
     */
}

Arguments

  • client: A reference to the client that is trying to join the room.
  • options: Options provided by the frontend SDK.
  • context: The request context, containing:
    • .token - the authentication token sent by the client.
    • .headers - the headers sent by the client.
    • .ip - the IP address of the client.

See Authentication for more details.


On Join

Triggered when a client successfully joins the room, after a successful onAuth.

Parameters:

  • client: A reference to the client that joined the room.
  • options: Options provided by the Client SDK. (Merged with default values provided by Server#define())
  • auth: (optional) auth data returned by onAuth method.
MyRoom.ts
async onJoin (client, options, auth?) {
    /**
     * This is a good place to add the client to the state.
     */
}

See Client SDK on .joinOrCreate() and .join().


On Drop

New in 0.17

Triggered when a client disconnects unexpectedly (without consent). This is the recommended place to use allowReconnection().

Parameters:

  • client: The client that was dropped from the room.
  • code: (optional) The close code of the leave event.
MyRoom.ts
async onDrop (client, code) {
    await this.allowReconnection(client, 20);
}

If onDrop is not defined, onLeave will be called for both consented and non-consented disconnections.


On Reconnect

New in 0.17

Triggered when a client successfully reconnects to the room after calling allowReconnection().

Parameters:

  • client: The client that reconnected to the room.
MyRoom.ts
onReconnect (client) {
    /**
     * Client has reconnected successfully.
     */
    console.log(client.sessionId, "reconnected!");
}

On Leave

Triggered when a client effectively leaves the room.

  • If onDrop is defined: onLeave is called only for consented disconnections (client called .leave()).
  • If onDrop is not defined: onLeave is called for both consented and non-consented disconnections.

Parameters:

  • client: The client that left the room.
  • code: (optional) The close code of the leave event.
MyRoom.ts
async onLeave (client, code) {
    /**
     * This is a good place to remove the client from the state.
     */
}

You may define this function as async:

onLeave(client, code) {
    if (this.state.players.has(client.sessionId)) {
        this.state.players.delete(client.sessionId);
    }
}

At Graceful Shutdown, onLeave is called with code = CloseCode.SERVER_SHUTDOWN for all clients.


On Dispose

The onDispose() method is called before the room is destroyed, which happens when:

  • there are no more clients left in the room, and autoDispose is set to true (default)
  • you manually call .disconnect().
MyRoom.ts
async onDispose() {
    /**
     * This is a good place to perform cleanup tasks.
     */
}

You may define async onDispose() an asynchronous method in order to persist some data in the database. In fact, this is a great place to persist player’s data in the database after a game match ends.

At Graceful Shutdown, onDispose is called for all rooms.


On Unhandled Exception

Opt-in to catch unhandled exceptions in your room. This method is called when an unhandled exception occurs in any of the lifecycle methods.

MyRoom.ts
onUncaughtException (err: Error, methodName: string) {
    console.error("An error occurred in", methodName, ":", err);
    err.cause // original unhandled error
    err.message // original error message
}

See Exception Handling for more details.


On Before Patch

The onBeforePatch lifecycle hook is triggered before state synchronization, at patch rate frequency. (see patchRate)

MyRoom.ts
onBeforePatch(state) {
    /*
     * Here you can mutate something in the state just before it is encoded &
     * synchronized with all clients
     */
}

On Cache Room (devMode)

An optional hook to cache external data when devMode is enabled. (See restoring data outside the room’s state)

MyRoom.ts
export class MyRoom extends Room {
  // ...
 
  onCacheRoom() {
    return { foo: "bar" };
  }
}

On Restore Room (devMode)

An optional hook to reprocess/restore data which was returned and stored from the previous hook onCacheRoom when Development Mode is enabled.

MyRoom.ts
export class MyRoom extends Room {
  // ...
 
  onRestoreRoom(cachedData: any): void {
    console.log("ROOM HAS BEEN RESTORED!", cachedData);
 
    this.state.players.forEach(player => {
      player.method(cachedData["foo"]);
    });
  }
}

On Before Shutdown

The onBeforeShutdown lifecycle hook is called as part of the Graceful Shutdown process. The process will only truly shutdown after all rooms have been disposed.

By default, the room will disconnect all clients and dispose itself immediately.

You may customize how the room should behave during the shutdown process:

MyRoom.ts
onBeforeShutdown() {
    //
    // Notify users that process is shutting down, they may need to save their progress and join a new room
    //
    this.broadcast("going-down", "Server is shutting down. Please save your progress and join a new room.");
 
    //
    // Disconnect all clients after 5 minutes
    //
    this.clock.setTimeout(() => this.disconnect(), 5 * 60 * 1000);
}

See graceful shutdown for more details.


Message Handling

Room handlers have these methods available.

On Messages

Register callbacks to process messages sent by the frontend.

Signature
messages = {
    [type: string | number]: (client, payload) => void
}
  • The type argument can be either string or number.
  • You can only define a single callback per message type.

Callback for specific type of message

MyRoom.ts
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 callback as a fallback to handle other types of messages.

MyRoom.ts
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);
    }
}

Message input validation

You may provide a validation schema using the validate() helper with a Zod schema. If validation fails, the message will be ignored.

The validated and typed data will be passed as payload on the message handler.

MyRoom.ts
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 });
    })
}

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


Methods

Set Simulation Interval

Optional: Set a simulation interval that can change the state of the game. The simulation interval is your game loop. Default simulation interval: 16.6ms (60fps)

Signature
this.setSimulationInterval (callback[, milliseconds=16.6])
MyRoom.ts
onCreate () {
    this.setSimulationInterval((deltaTime) => this.update(deltaTime));
}
 
update (deltaTime) {
    // implement your physics or world updates here!
    // this is a good place to update the room state
}

Set Patch Rate

⚠️

Deprecated: Use the patchRate property instead.

Set frequency the patched state should be sent to all clients. Default is 50ms (20fps)

Signature
this.setPatchRate (milliseconds)

Set Private

Set the room listing as private - or revert it to public, if false is provided.

Signature
this.setPrivate (bool)

Set Metadata

Set metadata to this room. Each room instance may have metadata attached to it - the only purpose for attaching metadata is to differentiate one room from another when getting the list of available rooms via Lobby Room or Match-maker API.

Signature
this.setMetadata (metadata)
MyRoom.ts
onCreate(options) {
    this.setMetadata({ friendlyFire: options.friendlyFire });
}

Set Matchmaking

New in 0.17

Update multiple matchmaking/listing properties at once with a single persist operation. This is the recommended way to update room listing properties.

Parameters

  • updates: object containing the properties to update
    • metadata (optional): room metadata (merged with existing metadata)
    • private (optional): whether the room should be private
    • locked (optional): whether the room should be locked
    • maxClients (optional): maximum number of clients allowed
    • unlisted (optional): whether the room should be unlisted from matchmaking queries
MyRoom.ts
// Update multiple properties at once
await this.setMatchmaking({
    metadata: { difficulty: "hard", rating: 1500 },
    private: true,
    locked: true,
    maxClients: 10
});

Set Seat Reservation Time

⚠️

Deprecated: Use the seatReservationTimeout property instead.

Set the number of seconds a room can wait for a client to effectively join the room. You should consider how long your onAuth() will have to wait for setting a different seat reservation time. The default value is 15 seconds.

Signature
this.setSeatReservationTime (seconds)

Broadcast Message

Send a message to all connected clients.

Signature
this.broadcast (type, payload, options?)

Available options are:

  • except: a Client, or array of Client instances not to send the message to
  • afterNextPatch: waits until next patch to broadcast the message

Broadcasting a message to all clients:

MyRoom.ts
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.


Lock Room

Locking the room will remove it from the pool of available rooms for new clients to connect to.

Signature
this.lock ()

Unlock Room

Unlocking the room returns it to the pool of available rooms for new clients to connect to.

Signature
this.unlock ()

Allow Reconnection

Allow the client to reconnect into the room.

A reconnection may be made manually (via sdk.reconnect()), or automatically in case of temporary network issues.

The .allowReconnection() method should be called on onDrop() OR onLeave() method.

Signature
this.allowReconnection (client, seconds)
  • The allowReconnection() returns a Deferred<Client> instance.
  • The returned Deferred instance is a promise-like structure, you can forcibly reject the reconnection by calling .reject() on it. (see second example)
  • Deferred type can forcibly reject the promise by calling .reject() (see second example)

Example: Rejecting the reconnection after a 20 second timeout.

MyRoom.ts
async onDrop (client: Client, code: number) {
    // flag client as inactive for other users
    this.state.players.get(client.sessionId).connected = false;
 
    // allow disconnected client to reconnect into this room until 20 seconds
    await this.allowReconnection(client, 20);
}
 
onReconnect(client: Client) {
    // client returned! let's re-activate it.
    this.state.players.get(client.sessionId).connected = true;
}
 
onLeave (client: Client, code: number) {
    // Client left by consent, remove from state immediately
    this.state.players.delete(client.sessionId);
}

Has Reached Max Clients

Returns whether the sum of connected clients and reserved seats exceeds the maximum number of clients.

Signature
this.hasReachedMaxClients ()
MyRoom.ts
onCreate(options) {
    if (this.hasReachedMaxClients()) {
        console.log("Room is full!");
    }
}

Disconnect

Disconnect all clients, then dispose.

Signature
this.disconnect ()

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.

Signature
this.broadcastPatch ()

If you’d like to have control over when to broadcast patches, you can do this by disabling the default patch interval:

MyRoom.ts
// disable automatic patches
patchRate = null;
 
onCreate() {
    this.clock.setInterval(() => {
        // only broadcast patches if your custom conditions are met.
        if (yourCondition) {
            this.broadcastPatch();
        }
    }, 2000);
}

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

MyRoom.ts
// 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

MyRoom.ts
// ...
this.clients.forEach((client) => {
    if (client.userData.team === "red") {
        client.send("hello", "world");
    }
});
// ...

Getting a client by sessionId.

MyRoom.ts
// ...
const client = this.clients.getById("UEsBFUBhK");
// ...

maxClients

Maximum number of clients allowed to connect into the room. When room reaches this limit, it is locked automatically. Unless the room was explicitly locked by you via lock() method, the room will be unlocked as soon as a client disconnects from it.


patchRate

Frequency to send the room state to connected clients, in milliseconds. Default is 50ms (20fps).

class MyRoom extends Room {
    patchRate = 50;
}

autoDispose

Automatically dispose the room when last client disconnects. Default is true

You may disable this, and let the room stay in memory forever by

class MyRoom extends Room {
    autoDispose = false;
}

maxMessagesPerSecond

Maximum number of messages a client can send to the server per second. If a client sends more messages than this limit, it will be disconnected. Default is Infinity (disabled).

class MyRoom extends Room {
    maxMessagesPerSecond = 30;
}

seatReservationTimeout

Set the number of seconds a room can wait for a client to effectively join the room. You should consider how long your onAuth() will have to wait for setting a different seat reservation time. The default value is 15 seconds.

class MyRoom extends Room {
    seatReservationTimeout = 15;
}

You may set the COLYSEUS_SEAT_RESERVATION_TIME environment variable if you’d like to change the seat reservation time globally.


locked (read-only)

This property will change on these situations:

  • The maximum number of allowed clients has been reached (maxClients)
  • You manually locked, or unlocked the room using lock() or unlock().

clock

It is recommended to use the clock instance for setTimeout and setInterval methods, as timers and intervals are automatically cleared when the room is disposed - preventing memory leaks.

MyRoom.ts
// ...
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);
}
// ...

presence

The presence is used as a shared in-memory database for your cluster, and for pub/sub operations between rooms.

MyRoom.ts
// ...
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");
}
// ...

Client

The client instance from the backend is responsible for the transport layer between the server and the client. It should not be confused 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.

MyRoom.ts
// ...
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.

MyRoom.ts
// ...
onJoin(client, options) {
  client.userData = { team: (this.clients.length % 2 === 0) ? "red" : "blue" };
}
onLeave(client)  {
  console.log(client.userData.playerNumber);
}
// ...

auth

The client.auth property holds the data returned by the onAuth() method.

MyRoom.ts
onAuth(token, request) {
    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.

MyRoom.ts
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();
}

reconnectionToken

The unique token used for reconnection. This token is regenerated each time the client connects.

MyRoom.ts
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.

Signature
client.send(type, payload)

The type can be either a string or a number.

MyRoom.ts
//
// 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.

Signature
client.sendBytes(type, bytes)

The type can be either a string or a number.

This is useful if you’d like to manually encode a message, rather than the default encoding (MsgPack).

MyRoom.ts
//
// sending message of string type ("powerup")
//
client.sendBytes("powerup", [ 172, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33 ]);
 
//
// sending message of number type (1)
//
client.sendBytes(1, [ 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 4000 and 4999 (see table of WebSocket close codes)

Signature
client.leave(code?: number)

This will trigger room.onLeave event on the frontend.

Table of WebSocket close codes

Close code (uint16)CodenameInternalCustomizableDescription
0 - 999YesNoUnused
1000CLOSE_NORMALNoNoSuccessful operation / regular socket shutdown
1001CLOSE_GOING_AWAYNoNoClient is leaving (browser tab closing)
1002CLOSE_PROTOCOL_ERRORYesNoEndpoint received a malformed frame
1003CLOSE_UNSUPPORTEDYesNoEndpoint received an unsupported frame (e.g. binary-only endpoint received text frame)
1004YesNoReserved
1005CLOSED_NO_STATUSYesNoExpected close status, received none
1006CLOSE_ABNORMALYesNoNo close code frame has been received
1007Unsupported payloadYesNoEndpoint received inconsistent message (e.g. malformed UTF-8)
1008Policy violationNoNoGeneric code used for situations other than 1003 and 1009
1009CLOSE_TOO_LARGENoNoEndpoint won’t process large frame
1010Mandatory extensionNoNoClient wanted an extension which server did not negotiate
1011Server errorNoNoInternal server error while operating
1012Service restartNoNoServer/service is restarting
1013Try again laterNoNoTemporary server condition forced blocking client’s request
1014Bad gatewayNoNoServer acting as gateway received an invalid response
1015TLS handshake failYesNoTransport Layer Security handshake failure
1016 - 1999YesNoReserved for future use by the WebSocket standard.
2000 - 2999YesYesReserved for use by WebSocket extensions
3000 - 3999NoYesAvailable for use by libraries and frameworks. May not be used by applications. Available for registration at the IANA via first-come, first-serve.
4000 - 4999NoYesAvailable for applications

Next Steps

Last updated on