# How rooms hibernate and wake

> When a room hibernates, what is saved, what happens when a player connects to a hibernated room, and what it costs.

Rooms on IRT don't need to run when nobody is in them. An empty room
hibernates: its room state goes to object storage and it leaves its room
host. When a player connects again, the room wakes with its state as it
was. Your code doesn't save or load anything for this to work.

## When a room hibernates

- 30 seconds after its last connection closes, unless its alarm is due
  within 2 minutes; then it stays active until the alarm has run.
- When the room asks to, with `room.requestHibernate()`. Its open
  connections run `onLeave` and close with 4011, and their clients don't
  reconnect by themselves; the game can offer to rejoin.
- When its room host drains, for example to move rooms to a new build.
  A room whose definition has `sleep` goes to sleep instead, so its
  players stay connected.
- When it goes to sleep, with its players still connected; see
  [Sleeping rooms](#sleeping-rooms).

Hibernating runs the room's `onHibernate` hook, serializes the room and
writes its room state to object storage. A hibernated room costs no
room-hours. A room can stay hibernated indefinitely; only deleting it
removes its room state.

## What is saved

Everything the SDK tracks is room state:

- the room's entities, `room.state` and each player's private state,
- the room's tick,
- its alarm, from `room.setAlarm`,
- the state of a stepper that declares `save` and `load`.

Writes don't wait for hibernation. As a room runs, its changes stream out
of the VM to IRT's host daemon after each tick, and the host daemon
writes the whole room state to object storage every 30 seconds when it
has changed, and again on hibernate. Because the changes are already
outside the VM:

- a room host that crashes, or that the watchdog kills for blocking its
  event loop, restarts its rooms with nothing lost;
- a host, or IRT's host daemon on it, that fails loses the changes since
  the last write to object storage: at most 30 seconds, unless object
  storage can't be reached.

Anything you keep outside the SDK, in plain variables, isn't saved.
Rebuild it in `onWake`. [Room state and the managed store](/docs/room-state)
has the details: what's saved, the log and base it's written as, and its
32 MB limit.

## How a room wakes

A room wakes when a client connects to it, when the API sends it a
message or reads its state, or when its alarm is due. A room woken for
its alarm runs `onAlarm` straight after `onWake`, and with no one
connected hibernates again as soon as it's done. See
[Alarms](/docs/reference/room-sdk#alarms).

A room's first start goes the same way. A room created through the API,
with `irt rooms create` or by connecting to a new ID, is `hibernated`
until its first connection or API call, and its client sees `waking`
then too. The difference is only that it has no room state, so
`onCreate` runs instead of `onWake`.

`irt rooms list` shows only active rooms; `irt rooms list --all` shows
hibernated ones too.

1. The router sees a connection for a room that isn't active. It holds
   the connection and tells the client the room is waking. The client
   SDK emits a `waking` event, so you can show it.
2. The router picks where the room runs: a VM of your tenant that already
   runs the room's build and has room to spare, or a new VM.
3. The room's state is read from object storage and the room starts in a
   room host. `onWake` runs.
4. The router sends the client its welcome, and every connection held
   while the room woke joins. `connect` resolves once the client holds
   the full state; a client that was reconnecting emits `restored`.

However many players connect at once, a room wakes once.

### How fast

What a wake costs depends on where it lands:

- **In a room host that is already running.** Most wakes. Starting a room
  inside a running room host takes microseconds; what remains is reading
  the room state.
- **In a VM that has to start.** VMs restore from a base snapshot. In our
  measurements, a VM restores and its room host answers in about 35 ms.
- **In the first room host of a build.** The first room to start or
  wake on a build, such as the first after a deploy, starts that build's
  room host, which loads the room runtime and your bundle before the
  room starts. In our measurements that added about 70 to 200 ms, and
  more for a large bundle, for code that does work when its module
  loads, or on a busy host. Later rooms of the build reuse the room
  host.

End-to-end wake times, including reading room state from object storage,
haven't been measured yet and will be published when they are.

If a room hasn't woken after 10 seconds, the router closes held
connections with close code 4010 and the client SDK reconnects after a
backoff. The wake isn't cancelled, so a slow wake loses nothing.

## Sleeping rooms

A room whose players stay connected but rarely act, such as a board
game, a chat or a lobby waiting for players, can sleep: it hibernates
while the router holds its players' connections, and the next thing one
of them does wakes it. Nobody leaves, and time asleep costs no
room-hours.

```ts
import { defineCalls, defineRoom, defineSchema, entity } from "@irt/room";

const Board = entity("board", { turn: "u32" });
const schema = defineSchema({ state: Board, calls: defineCalls<{ move(): number }>() });

export default defineRoom({
  schema,
  sleep: { after: 30_000 },
  calls: {
    // A call made while the room sleeps wakes it, and is answered once it has.
    move(room) {
      room.state.turn += 1;
      return room.state.turn;
    },
  },
});
```

With `sleep.after`, the room sleeps once it has been quiet that long:
no message from any client but a ping, nothing sent or stored, no call
or server API request in flight. `room.sleep()` sleeps it at once.
Presence and each connection's `data` are saved with the room state, so
when a message, call or close from a held connection, a new connection,
the alarm or an API call wakes it, `onWake` runs with `slept: true` and
the connections are back, without `onJoin`. A held connection that
closed meanwhile gets `onLeave` then. Clients see `room.asleep` and the
`sleep` and `wake` events, and their calls are answered after the wake.

Opting in promises that the room keeps every deadline in its
[alarm](/docs/reference/room-sdk#alarms) rather than in timers or by
counting ticks, since nothing ticks while it sleeps. See
[Sleeping rooms](/docs/reference/room-sdk#sleeping-rooms) for the
details and limits. A wake is billed at least 10 seconds, so set
`sleep.after` long enough that a conversation doesn't wake the room
for every message.

A room whose definition has `sleep` also sleeps, rather than closing
its players' connections, when the server it runs on is taken out of
service, and wakes on another one when a player next acts. Players
whose connection drops while their room sleeps, such as when IRT
restarts the router they're connected to, reconnect to the sleeping
room without waking it, as long as their client still holds the room's
state from before the sleep.

## Timers across hibernation

Store deadlines in ticks, as `endsAt = room.tick + seconds * tickRate`
in room state, and compare in `onTick`. Ticks keep running for the 30
seconds between the last connection closing and the room hibernating,
then stop until it wakes, and `room.tick` carries on from where it
stopped. Left alone, a turn or round timer therefore runs for those 30
seconds with nobody there, then freezes: a round left 20 seconds before
its end expires unwatched, and one left 40 seconds before resumes with
10 left. Bots and AI turns driven from `onTick` play on in that window
too. Pick one of two behaviours instead.

**Freeze while empty.** The room is empty when `onLeave` sees
`room.connections.size === 0`, and occupied again when `onJoin` sees
`room.connections.size === 1`: the connection map already lacks the
leaving connection in `onLeave` and holds the joining one in `onJoin`.
Move what's left of the timer into room state there, and back on the
first join. `onLeave` runs for every open connection before any
hibernation but a sleep, so this covers `requestHibernate` and drains
too. Gate bots
the same way, or on `room.connections.size > 0` in `onTick`.

```ts
// round-timer.ts
import { defineRoom, defineSchema, entity } from "@irt/room";

export const Round = entity("round", { endsAt: "u32", pausedLeft: "u32" });
export const schema = defineSchema({ state: Round });

const ROUND_SECONDS = 60;

export default defineRoom({
  schema,
  onCreate(room) {
    room.state.endsAt = room.tick + ROUND_SECONDS * room.tickRate;
  },
  onJoin(room) {
    if (room.connections.size === 1 && room.state.pausedLeft > 0) {
      room.state.endsAt = room.tick + room.state.pausedLeft;
      room.state.pausedLeft = 0;
    }
  },
  onLeave(room) {
    if (room.connections.size === 0 && room.state.endsAt > room.tick) {
      room.state.pausedLeft = room.state.endsAt - room.tick;
      room.state.endsAt = 0;
    }
  },
  onTick(room) {
    if (room.state.endsAt !== 0 && room.tick >= room.state.endsAt) {
      room.state.endsAt = 0;
      room.broadcast({ type: "round-over" });
    }
  },
});
```

```ts
// round-timer.test.ts
import { testRoom } from "@irt/testing";
import { expect, it } from "vitest";
import room from "./round-timer.js";

it("freezes the round while the room is empty", async () => {
  const t = testRoom(room);
  const alice = await t.connect({ player: "alice" });
  await t.host.tick(50 * 20);
  await alice.leave();
  await t.host.advance(31_000);
  expect(t.status).toBe("hibernated");

  const back = await t.connect({ player: "alice" });
  await t.host.tick(5 * 20);
  expect(back.messages).toEqual([]);
  await t.host.tick(6 * 20);
  back.expectMessage({ type: "round-over" });
});
```

**Catch up with wall time.** Let the timer run through the idle window,
and on wake move each deadline back by the time the room spent
hibernated, which `onWake` gets as `elapsed` milliseconds. A long
absence can reset the game instead:

```ts
// catch-up.ts
import { defineRoom, defineSchema, entity } from "@irt/room";

const Round = entity("round", { endsAt: "u32", lobby: "bool" });

export default defineRoom({
  schema: defineSchema({ state: Round }),
  onWake(room, { elapsed }) {
    if (elapsed > 30 * 60_000) {
      room.state.lobby = true;
      room.state.endsAt = 0;
    } else if (room.state.endsAt !== 0) {
      room.state.endsAt = Math.max(room.tick, room.state.endsAt - Math.round((elapsed / 1000) * room.tickRate));
    }
  },
});
```

Room code's own timers, such as `setTimeout`, are neither: they don't
survive hibernation and don't follow the test clock.

## Offline progress

An idle or incremental game keeps producing while nobody is there, and
the room hibernates then, so nothing ticks. Don't add up production per
tick. Store each amount with the wall time it's valid at, and compute it
in closed form at any later time:

```ts
// colony.ts
import { defineRoom, defineSchema, entity } from "@irt/room";

const Colony = entity("colony", { food: "f64", rate: "f64", cap: "f64", at: "f64" });

const foodAt = (c: { food: number; rate: number; cap: number; at: number }, now: number) =>
  Math.min(c.cap, c.food + (c.rate * (now - c.at)) / 1000);

export default defineRoom({
  tickRate: 1,
  schema: defineSchema({ state: Colony }),
  onCreate(room) {
    Object.assign(room.state, { food: 0, rate: 0.5, cap: 1000, at: room.now() });
  },
  onMessage(room, _conn, msg) {
    if ((msg as { type?: string }).type !== "spend") return;
    // Settle the amount only when it changes, so nothing is written per tick.
    const now = room.now();
    room.state.food = foodAt(room.state, now) - 10;
    room.state.at = now;
  },
});
```

- `room.now()` is wall-clock milliseconds, the same as `Date.now()` once
  deployed (each VM's clock is set from its host's when it boots), but
  it also follows `@irt/testing`'s clock, so a test can move it by days.
  Use it rather than `Date.now()` for anything timed in real time, and
  rather than a clock built from `room.tick`, which stops while the room
  is hibernated and changes meaning when a build changes `tickRate`.
- Clients compute the same function from `at` to show live amounts, so
  nothing has to tick for the display either.
- Events on a wall-clock schedule, such as something every 4 hours of
  UTC or a reset at midnight, are counted between the stored time and
  now: `Math.floor(now / period) - Math.floor(at / period)`.
- `onWake` gets `{ elapsed, savedAt, reason }`: `savedAt` is the wall time room
  state was last written, at the room's last tick with changes or its
  hibernation, and `elapsed` the milliseconds from then to the wake,
  never negative. A game that computes from `at` needs neither, but
  they suit a "while you were away" summary.

## Retention

`rooms.retention` in [`irt.config.ts`](/docs/cli#irtconfigts) sets how
long a room is kept after it hibernates:

| Value | Meaning |
|---|---|
| `"none"` | The room is deleted, not hibernated, when its idle timeout passes with no connections. Its room state is still saved while it's active, so a crashed host loses nothing. |
| `"<n>h"`, `"<n>d"` | The room is deleted after it has been hibernated that long without waking, up to `"365d"`. A wake restarts the clock at its next hibernation. |
| `"forever"` | The room is never deleted except through the API. |

The default is `"7d"`. A deleted room's ID can be created again; a
client connecting to a deleted room gets close code 4002, or creates it
again as a new room when `players.createRooms` is on, and webhooks get
`room.deleted` with reason `expired`.

A room created through the [HTTP API](/docs/http-api#rooms) or
`irt rooms create --retention` can have a retention of its own, which
wins over the project's; rooms clients create use the project's.

## Generations

Each wake gives the room a new generation. A client drops all its
entities and prediction state on a change of generation and on any
reconnect, then receives a full state update. Your client code sees
entities leave and enter again.

## New builds

When a build is made live, the project's `rooms.buildUpdate` setting decides
what running rooms do:

- `hibernate` (the default): rooms stay on their build until they next
  hibernate, and wake on the new one.
- `restart`: rooms hibernate, saving their room state, and wake on the
  new build straight away. Clients reconnect automatically, with close
  code 4008. Each host moves 20 rooms a second, so a project with many
  rooms finishes over a minute or so rather than all at once. A room with
  no connections wakes on the new build when something next connects.

When a build changes the shape of room state, it provides an up migration,
which runs when a room wakes on it. The room state from before the
migration is kept for 7 days, so a rollback can restore it.

A rollback makes an older build live again. A room whose room state is
at a newer version than the build's wakes from its room state from
before the migration, losing progress since then, and the project gets
a `room.restored` webhook. A room with none kept, because it was created
on the newer build or its kept state is over 7 days old, fails to wake,
with the reason in `irt logs`, until a build at its version is live
again. See [`irt rollback`](/docs/cli#irt-rollback-build-id). See also
[Builds and deploys](/docs/builds).

## What it costs

A room is billed in room-hours by room size, from the start of a wake
until hibernation completes. Many rooms share a room host, and many room
hosts share a VM, so an idle room doesn't hold a process or a VM of its
own. A VM stops 5 minutes after its last room hibernates.

This is what makes per-room pricing cheap: you pay for rooms while
players are in them, not for servers that wait for players.
