Guides

Multiplayer: rooms, lobbies & .io-style games

AdminUpdated Sep 19, 2026

Multiplayer: rooms, lobbies & .io-style games

Opt in by publishing with realtime: true. The relay then accepts connections from your game's sandbox and fans small payloads between players in a room.

Rooms are lobbies

A room is just a name you choose — there's nothing to provision. Rooms are created on first join, scoped to your game (two games can both use "lobby-1" without colliding), and destroyed when the last player leaves.

const room = GameSDK.connectRoom({ room: "lobby-1", name: "Ana" });

room.on("joined", ({ peerId, peers, snapshots }) => {
  // `snapshots` is every peer's latest state — draw the world immediately.
});
room.on("peerJoined", ({ peerId, name }) => {});
room.on("peerLeft",   ({ peerId }) => {});
room.on("state",      ({ peerId, data }) => {});  // another player's setState
room.on("message",    ({ peerId, data }) => {});  // another player's send

room.setState({ x, y, mass });   // your latest state (kept for late joiners)
room.send({ type: "shoot" });    // a one-off event (not retained)
room.close();

Lobby browser + matchmaking

GameSDK.listRooms() returns your game's active rooms with live player counts (busiest first) — enough to draw a lobby list or auto-place a player:

const { rooms, maxPeersPerRoom } = await GameSDK.listRooms();
// rooms: [{ room: "lobby-1", peers: 61 }, { room: "lobby-2", peers: 12 }]

// Matchmaking: fill the busiest room that still has space, else open a new one.
const open = rooms.find((r) => r.peers < maxPeersPerRoom);
const target = open ? open.room : `lobby-${rooms.length + 1}`;
const room = GameSDK.connectRoom({ room: target });

// Always handle a full/limited room — another player may take the last slot
// between your list and your join.
room.on("error", ({ code }) => {
  if (code === "room_full" || code === "game_room_limit") retryWithAnotherRoom();
});

⚠️ The relay is NOT authoritative — design for it

The server fans out messages; it never runs your game logic. Every client broadcasts its own state and the others believe it. That's fine for co-op, racing, drawing, or social games — but for anything competitive (agar.io-style eating, PvP damage, leaderboard-relevant scoring) a modified client can simply claim any position, mass, or score.

For competitive games use a host-authoritative model:

  1. Pick a host deterministically — e.g. the lowest peerId in the room.

  2. Everyone else sends inputs (room.send({ up: true })), not state.

  3. The host runs the simulation and broadcasts the world with setState().

  4. Handle host migration: on peerLeft, recompute the host; the new host seeds from its last known snapshot.

This also scales far better. 64 players each broadcasting 30×/sec is ~120k fan-out sends per second per room; one host broadcasting the world at 20 Hz is a tiny fraction of that. Send deltas, not the whole world, and keep state under the payload cap.

Limits (per game, enforced server-side)

Limit

Value

Players per room

64

Rooms per game

200

Message size

8 KB

Messages per second, per player

30

Retained snapshot per player

8 KB

Concurrent connections per IP

20

Exceeding them returns an error event (room_full, game_room_limit, rate_limited, too_large) rather than dropping you silently — handle those events and degrade gracefully.


Was this page helpful?