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

**Simple**

```ts filename="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!");
    }
}
```

**With Full Type Safety**

You may specify the `metadata`, `state`, and `client` types for full type safety.

```ts filename="MyRoom.ts"
import { Room, type Client as MyClient } from "colyseus";
import { MyState } from "./MyState";

interface MyMetadata {
    difficulty: string;
    rating: number;
}

type Client = MyClient<{
    // specify the shape of outgoing messages to the client
    // so client.send() is type-checked
    // room.onMessage() on the frontend also becomes type-checked
    messages: {
        welcome: string;
    }
}>;

export class MyRoom extends Room<{
    state: MyState,
    metadata: MyMetadata,
    client: Client
}> {
    state = new MyState();

    onCreate(options: MyMetadata) {
        this.metadata = { difficulty: options.difficulty, rating: options.rating };
    }

    onJoin(client: Client, options: any) {
        client.send("welcome", "Welcome to the room!");
    }
}
```

## Room State

Set the synchronizable room state. See [State Synchronization](https://docs.colyseus.io/state.md) and [Schema](https://docs.colyseus.io/state/schema.md) for more details.

```typescript
import { Room } from "colyseus";
import { MyState } from "./MyState";

export class MyRoom extends Room {
    state = new MyState();
}
```

> **Warning:**
>
> 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

```ts filename="MyRoom.ts"
messages = {
    "action": (client, payload) => {
        console.log(client.sessionId, "sent 'action' message: ", payload);
    }
}
```

> **Note:**
>
> 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.

```ts filename="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.

```ts filename="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()`](https://docs.colyseus.io/sdk.md) from the client SDK, skipping the default MsgPack decoding.

```ts filename="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.

```ts filename="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)`](https://docs.colyseus.io/sdk.md#requestresponse). 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.

```ts filename="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](https://docs.colyseus.io/sdk.md#requestresponse).
- `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](#fallback-for-all-messages) 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](https://docs.colyseus.io/room/lifecycle.md)** 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`:

```ts filename="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
}
```

> **Note:**
>
> `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](https://docs.colyseus.io/netcode/server-input.md).

---

### 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](https://docs.colyseus.io/matchmaker/visibility.md)** 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`**: a [`Client`](https://docs.colyseus.io/room.md#client-instance), or array of `Client` instances not to send the message to
- **`afterNextPatch`**: waits until next patch to broadcast the message

**Broadcast to all**

Broadcasting a message to all clients:

```ts filename="MyRoom.ts"
messages = {
    "action": (client, payload) => {
        // broadcast a message to all clients
        this.broadcast("action-taken", "an action has been taken!");
    }
}
```

**Broadcast except for sender**

Broadcasting a message to all clients, except the sender.

```ts filename="MyRoom.ts"
messages = {
    "fire": (client, payload) => {
        // sends "fire" event to every client, except the one who triggered it.
        this.broadcast("fire", payload, { except: client });
    }
}
```

**Broadcast after state patch**

Broadcasting a message to all clients, only after a change in the state has been applied:

```ts filename="MyRoom.ts"
messages = {
    "destroy": (client, payload) => {
        // perform changes in your state!
        this.state.destroySomething();

        // this message will arrive only after new state has been applied
        this.broadcast("destroy", "something has been destroyed", { afterNextPatch: true });
    }
}
```

> **Note:**
>
> The client will receive the message in the [`onMessage()`](https://docs.colyseus.io/sdk.md#receiving-messages) callback.

---

### Broadcast Message (in bytes)

Send a raw byte array to all connected clients, skipping the default MsgPack encoding. The counterpart of [`client.sendBytes()`](#send-message-in-bytes) for broadcasts.

```ts filename="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.

```ts filename="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](https://docs.colyseus.io/room/reconnection.md). Pass the client's `reconnectionToken` as the second argument to check a reconnection reservation specifically.

```ts filename="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](https://docs.colyseus.io/sdk/connection.md#leaving-a-room). The default is `CONSENTED` (`4000`); custom application codes use `4011`–`4999`. See the [close-code table](#table-of-websocket-close-codes).

```ts filename="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

> **Warning:**
>
> **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:

```ts filename="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](https://docs.colyseus.io/room/reconnection.md)** 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.

> **Note:**
>
> You may customize the Room ID during `onCreate()` by setting the `this.roomId` property. See [Recipes &raquo; Customize Room ID](https://docs.colyseus.io/recipes/custom-room-id.md)

---

### `roomName`

The name of the room you provided in the `rooms` configuration of [`defineServer()`](https://docs.colyseus.io/server.md#define-room-type).

---

### `state`

The synchronized state of the room.

> **Note:**
>
> See [State Synchronization](https://docs.colyseus.io/state.md).

---

### `metadata`

The room's matchmaking metadata. This data is used to filter rooms during matchmaking queries.

```ts filename="MyRoom.ts"
// Get metadata
console.log(this.metadata.difficulty);

// Set metadata during onCreate() only
onCreate(options) {
    this.metadata = { difficulty: "hard", rating: 1500 };
}
```

> **Warning:**
>
> The `metadata` setter can only be used during `onCreate()`. To update metadata after the room is created, use [`setMetadata()`](https://docs.colyseus.io/matchmaker/visibility.md#matchmaking-properties) or [`setMatchmaking()`](https://docs.colyseus.io/matchmaker/visibility.md#matchmaking-properties) instead.

---

### `clients`

The array of connected clients. See [Client Instance](#client-instance).

#### Sending a message to a specific client

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

#### Getting a client by `sessionId`.

```ts filename="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`.

```ts filename="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);
}
// ...
```

> **Note:**
>
> See [Timing Events](https://docs.colyseus.io/room/timing-events.md).

---

### `presence`

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

```ts filename="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");
}
// ...
```

> **Note:**
>
> See [Presence API](https://docs.colyseus.io/server/presence.md).

---

## 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](https://docs.colyseus.io/sdk.md), as they have completely different purposes.

You operate on `client` instances from [`this.clients`](#clients), [`Room#onJoin()`](https://docs.colyseus.io/room/lifecycle.md#on-join), [`Room#onLeave()`](https://docs.colyseus.io/room/lifecycle.md#on-leave) and [`Room#onMessage()`](#on-messages).

### Properties

#### `sessionId`

Unique identifier of the client connection.

```ts filename="MyRoom.ts"
// ...
onJoin(client, options) {
    console.log(client.sessionId);
}
// ...
```

> **Note:**
>
> In the frontend, you can find the [`sessionId` in the `room` instance](https://docs.colyseus.io/sdk.md#room-reference).

---

#### `userData`

The `client.userData` can be used to store player-specific data easily accessible via the `client` instance. This property is meant for convenience.

```ts filename="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()`](https://docs.colyseus.io/room/lifecycle.md#on-auth) method.

```ts filename="MyRoom.ts"
onAuth(client, options, context) {
    return { userId: "123" };
}

onJoin(client, options) {
    console.log(client.auth.userId);
}
```

> **Note:**
>
> See [Authentication](https://docs.colyseus.io/auth.md) 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.

```ts filename="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();
}
```

> **Note:**
>
> See [ State Synchronization → State View](https://docs.colyseus.io/state/view.md) for more details.

---

#### `reconnectionToken`

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

```ts filename="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`.

```ts filename="MyRoom.ts"
//
// sending message of string type ("powerup")
//
client.send("powerup", { kind: "ammo" });

//
// sending message of number type (1)
//
client.send(1, { kind: "ammo"});
```

> **Note:**
>
> [See how to handle `onMessage` from the frontend SDK](https://docs.colyseus.io/sdk.md#receiving-messages)

---

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

```ts filename="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 `4000`–`4010` are reserved by the framework. See the [table of WebSocket close codes](#table-of-websocket-close-codes) below.

```ts filename="MyRoom.ts"
// disconnect this client with the default code (4000, CONSENTED)
client.leave();

// disconnect this client with a custom code
client.leave(4020);
```

> **Note:**
>
> This call will trigger the [`room.onLeave`](https://docs.colyseus.io/sdk/connection.md#leaving-a-room) 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:

- [Lobby Room](https://docs.colyseus.io/matchmaker/lobby.md): A live listing of available rooms, the backend for a room browser UI.
- [Queue Room](https://docs.colyseus.io/matchmaker/queue.md): A matchmaking queue that groups players over time and spawns a match room when a group is ready.
- [Relay Room](https://docs.colyseus.io/room/relay.md): A lightweight relay that broadcasts client messages to everyone else, with no authoritative state.

## Next Steps

- [State Synchronization](https://docs.colyseus.io/state.md) - Learn how to define and synchronize room state
- [Server Configuration](https://docs.colyseus.io/server.md) - Configure your Colyseus server
- [Timing Events](https://docs.colyseus.io/room/timing-events.md) - Schedule delayed and recurring events

---
Source: https://docs.colyseus.io/room
