Rooms and Broadcast
The broadcast — one event reaching many clients at once — is the reason WebSocket servers exist. This lesson covers the two flavours of broadcast (to everyone and to a room) and the data structures that make rooms work.
Broadcasting to everyone
Section titled “Broadcasting to everyone”The ws library gives you wss.clients, a Set of every currently open socket. To send something to all of them, iterate and send — checking that each socket is actually open first:
import { WebSocket } from 'ws';
function broadcastAll(wss, message) { for (const client of wss.clients) { if (client.readyState === WebSocket.OPEN) { client.send(message); } }}The readyState === WebSocket.OPEN guard matters: a socket can be mid-close or already closing, and calling send on it throws or silently buffers. Always filter to OPEN before sending. This is the simplest possible broadcast — useful for a global announcement, but a sledgehammer for anything targeted.
Rooms: broadcasting to a subset
Section titled “Rooms: broadcasting to a subset”Most apps do not want everyone to hear everything. A chat message belongs to one channel; a game update belongs to one match. The pattern is a room — a named group of sockets. The classic data structure is a Map from room name to a Set of sockets:
const rooms = new Map(); // roomName -> Set<ws>
function join(room, ws) { if (!rooms.has(room)) rooms.set(room, new Set()); rooms.get(room).add(ws);}
function leave(room, ws) { rooms.get(room)?.delete(ws); if (rooms.get(room)?.size === 0) rooms.delete(room); // tidy up empty rooms}
function broadcastRoom(room, message) { for (const client of rooms.get(room) ?? []) { if (client.readyState === WebSocket.OPEN) client.send(message); }}Now a message fans out only to the sockets that joined that room — not the whole server. Notice the symmetry with the previous lesson: each socket’s state.subscriptions set is the client’s view of its rooms, while this rooms map is the server’s reverse index. Keep the two in sync.
flowchart TB SENDER["client A<br/>(in room 'sports')"] -- "publish to 'sports'" --> S["server"] S --> ROOM["room 'sports'"] ROOM == "deliver" ==> B["client B ✓ in sports"] ROOM == "deliver" ==> C["client C ✓ in sports"] S -. "not delivered" .-> D["client D ✗ in 'news'"]
Client D, which joined a different room, never hears the message — that selectivity is the entire point of rooms.
Don’t echo to the sender (usually)
Section titled “Don’t echo to the sender (usually)”A common refinement: when broadcasting a chat line, you often skip the sender, because their own client already showed the message optimistically. Just compare against the sending socket:
for (const client of rooms.get(room) ?? []) { if (client !== sender && client.readyState === WebSocket.OPEN) { client.send(message); }}Whether to include the sender is an application choice — some apps echo back so the sender’s UI confirms the server received it. Either way, the client !== sender toggle is how you control it.
A live broadcast server
Section titled “A live broadcast server”The demo below implements rooms on a real ws server. A client sends {"type":"join","room":"x"} to join, then {"type":"say","text":"hi"} to broadcast to everyone in its room. Cleanup on close removes the socket from its room so you never broadcast to a ghost. Open it in StackBlitz and connect a couple of clients to the same room.
Needs the Node.js runtime — open in StackBlitz to run.
With two clients joined to the same room, a say from one arrives at the other but not back at the sender (we passed ws as the except argument). A third client in a different room hears nothing. That targeted fan-out — backed by a Map of Sets and disciplined cleanup — is the core engine of every chat, presence, and live-collaboration feature you will build.