Rooms

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.

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

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 type argument can be either string or number.
  • You can only define a single handler per message type.

Handler 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 handler 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);
    }
}

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.

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

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

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 });
    })
}

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.

MyRoom.ts
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 Promise works.
  • If the handler throws or its Promise rejects, the client’s request rejects with that error instead of timing out.
  • Every handler also receives a third ctx argument. Call ctx.reject(reason) to settle the request as rejected: a deliberate business-logic “no”, distinct from an error. On the client, the rejection arrives as an Error with name: "rejected" and the raw reason on its .reason property. See Request/Response on the SDK.
  • ctx.resolve(value) answers the request explicitly. Use it when the handler must keep working after replying. A plain return is 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 a no_handler error.

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:

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

PropertyDefaultDescription
maxClientsInfinityMaximum number of clients allowed. Room is auto-locked when full.
patchRate50Frequency to send state updates to clients, in milliseconds (20fps).
autoDisposetrueAutomatically dispose the room when last client disconnects.
maxMessagesPerSecondInfinityMaximum messages a client can send per second. Exceeding disconnects the client.
seatReservationTimeout15Seconds 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: 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.


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.

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

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

MyRoom.ts
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 40114999. See the close-code table.

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

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);
}

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.

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

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

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

auth

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

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

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.

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.

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

MyRoom.ts
//
// 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 40004010 are reserved by the framework. See the table of WebSocket close codes below.

MyRoom.ts
// 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)CodenameInternalCustomizableDescription
0 - 999YesNoUnused
1000NORMAL_CLOSURENoNoSuccessful operation / regular socket shutdown
1001GOING_AWAYNoNoClient is leaving (browser tab closing)
1002Protocol errorYesNoEndpoint received a malformed frame
1003Unsupported frameYesNoEndpoint received an unsupported frame (e.g. binary-only endpoint received text frame)
1004YesNoReserved
1005NO_STATUS_RECEIVEDYesNoExpected close status, received none
1006ABNORMAL_CLOSUREYesNoNo 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
1009Frame 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.
4000CONSENTEDNoNoClient left intentionally (room.leave())
4001SERVER_SHUTDOWNNoNoServer graceful shutdown (production)
4002WITH_ERRORNoNoClosed due to an error
4003FAILED_TO_RECONNECTNoNoAll reconnection attempts failed, or the server denied/failed the reconnection
4010MAY_TRY_RECONNECTNoNoServer shutdown in dev mode (allows reconnect)
4011 - 4999NoYesAvailable 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:

Next Steps