Real-Time Backend
Rooms and Channels
Discord serves 800K members in a single guild. How does the server know to send a message to #general or to #gaming? Without the rooms abstraction every emit would have to address millions of connections by hand.
- Discord guilds: each text channel is a separate room. A member gets push only from channels they are present in. 19M active servers run on this abstraction.
- Slack channels: the 50K-member cap follows from broadcast math: at 50K sockets one emit produces 5MB of traffic. Slack keeps channel membership in PostgreSQL and active connections in Redis.
- Twitch raid: a raid moves 50K viewers between channels atomically. At the WebSocket layer that is 50K parallel leave+join. Redis Cluster handles it via a Lua script in one transaction.
- Google Docs: each document is a room. Operational transforms broadcast only to room members. When the last editor closes the tab, the room is removed from memory (the document stays in the DB).
Rooms Concept
A **room** is a named group of connections sharing a common context. The server holds a `roomId -> Set<socketId>` map and uses it for targeted delivery. A client by itself does not know who else is in the room. Only the server does.
Discord implements this model at scale: a server (guild) with 800K+ members is a hierarchy of rooms. Each text channel is a separate room. A member receives messages only from channels their connection is present in. Voice channels are a separate type of room with extra metadata (bitrate, region).
A room is a server-side abstraction. At the transport layer there are no 'rooms'. There are only individual WebSocket connections. All routing is done by the server, iterating the `Set<socketId>` and sending to each one individually.
- Room (namespaced) — Created and destroyed dynamically. Members come and go. Example: game session, chat room, live editor session.
- Channel (persistent topic) — Exists independently of subscribers. A Slack channel with 50K+ members exists even when everyone is offline. Example: pub/sub topic, Discord #general.
What happens to a Socket.io room when its last member leaves?
Rooms Join Leave
Join and leave are mutations of the server-side Set. `socket.join('room-id')` adds `socket.id` to `adapter.rooms.get('room-id')`. `socket.leave('room-id')` removes it. On disconnect the server automatically calls leave for every room of that socket.
A Twitch raid is a vivid case of atomic join: 50K viewers leave one channel and enter another simultaneously. At the WebSocket layer this is 50K parallel join operations. The Redis adapter does it via a Lua script for atomicity: one SMOVE instead of N separate operations.
- `socket.join(room)`: adds the socket to a room (async in cluster mode)
- `socket.leave(room)`: removes the socket from a room
- `socket.rooms`: Set of every room the socket is currently in
- `io.socketsLeave(room)`: force-kick everyone from a room (admin operation)
- `io.in(room).fetchSockets()`: list every socket in a room
In Socket.io: `socket.to('room').emit(...)` vs `io.to('room').emit(...)`. What is the difference?
Rooms Broadcast
Broadcast is sending one message to every member of a room. On a single server it is a loop over `Set<socketId>` calling `socket.send()` for each one. In a cluster with multiple servers the message has to reach every node, which is what pub/sub via Redis or another broker is for.
Slack caps channels at 50K members. That is not a random number. Broadcasting to a 50K-member channel from one server makes 50K write() syscalls. At 100 bytes per message that is 5MB of outbound traffic per emit. At 10 events/sec the channel produces 50MB/s, right at the edge of saturating a 1Gbps link.
| Pattern | Method | When to use |
|---|---|---|
| Everyone in room | `io.to(room).emit()` | State sync, announcements |
| Everyone except self | `socket.to(room).emit()` | Chat, player actions |
| Several rooms | `.to(r1).to(r2).emit()` | Cross-room notifications |
| Exclude a room | `.except(room).emit()` | Mute, ban mechanics |
| Without guarantee | `.volatile.to(room).emit()` | Positions, cursors at 60fps |
A game sends player positions 60 times per second. Which broadcast pattern fits?
Rooms Impl
Implementing rooms without a framework is `Map<string, Set<string>>` on the server. Socket.io adds an adapter layer on top: an in-memory adapter for a single server, Redis/Postgres adapters for a cluster. The adapter syncs room state between instances via pub/sub.
In a cluster a two-way index is needed: `roomId -> [serverId, socketId][]`. Redis Cluster stores it as two hashes: `room:{id}:members` and `socket:{id}:rooms`. Join/leave updates both atomically via a Lua script. That is how `@socket.io/redis-adapter` works: it pub/subs broadcast commands between instances.
A room is a server process or a separate thread that handles its members' messages
A room is just a Set<socketId> in memory. There are no separate processes. It is a pure data structure for addressing.
Confusion comes from the term: a real-life room is a physical place with walls. In a realtime backend a room is a routing label. Broadcast is just `for (id of room) socket.send()` in the same process's event loop.
Why does a room manager need a reverse index `socketId -> Set<roomId>`?
Key ideas
- A room is `Map<roomId, Set<socketId>>` on the server. Not a process, not a thread, just a data structure for routing.
- Join/leave are Set mutations. Disconnect auto-calls leave for every room of the socket. The reverse index `socketId -> rooms` is critical for O(1) cleanup.
- `socket.to(room)` excludes the sender; `io.to(room)` includes. For 60fps updates use volatile emit to avoid buffer overflow.
- In a cluster, rooms are synced via Redis pub/sub. Every broadcast is published to Redis, every instance receives it and delivers to its local sockets.
Related topics
Rooms sit on top of WebSocket and scale via cluster primitives.
- WebSocket Protocol — Transport layer beneath rooms. A room is a server abstraction; WebSocket knows nothing about rooms.
- Pub/Sub Pattern — Mechanism for syncing rooms across cluster instances. Redis pub/sub relays broadcast commands between servers.
- Horizontal Scaling — Without a Redis adapter, rooms exist on only one instance. Horizontal scaling requires shared state.
Вопросы для размышления
- A chat app stores message history. Where should the list of channel members live: in Redis (alongside active sockets) or only in PostgreSQL?
- A user opens one browser tab and logs in from two devices. Should you create two separate personal rooms, or merge them under userId?
- A game server sends every player's position 60 times per second. With 100 players in a room that is 100 x 100 = 10K messages/sec. How to optimize without giving up broadcast?