WebSockets Cheatsheet
Rooms and Channels
Use this WebSockets reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
Room Concept
A room (also called a channel or group) is a named subset of connected clients. The ws library has no built-in room system — implement with a Map<string, Set<WebSocket>>.
// rooms: Map<roomId, Set<WebSocket>> const rooms = new Map(); function getRoom(roomId) { if (!rooms.has(roomId)) rooms.set(roomId, new Set()); return rooms.get(roomId); }
Joining and Leaving
wss.on("connection", (ws, req) => { const roomId = new URL(req.url, "http://x").searchParams.get("room") ?? "general"; ws.roomId = roomId; getRoom(roomId).add(ws); // Notify room on join broadcastRoom(roomId, ws, { type: "join", userId: ws.userId }); ws.on("close", () => { getRoom(roomId).delete(ws); if (getRoom(roomId).size === 0) rooms.delete(roomId); // GC empty rooms broadcastRoom(roomId, null, { type: "leave", userId: ws.userId }); }); });
Broadcasting Within a Room
function broadcastRoom(roomId, exclude, data) { const room = rooms.get(roomId); if (!room) return; const frame = typeof data === "string" ? data : JSON.stringify(data); for (const client of room) { if (client !== exclude && client.readyState === WebSocket.OPEN) { client.send(frame); } } } // Include sender function broadcastRoomAll(roomId, data) { broadcastRoom(roomId, null, data); }
Multi-Room Membership (one socket, many rooms)
// Store rooms per socket as a Set wss.on("connection", (ws) => { ws.rooms = new Set(); }); function joinRoom(ws, roomId) { getRoom(roomId).add(ws); ws.rooms.add(roomId); } function leaveRoom(ws, roomId) { getRoom(roomId).delete(ws); ws.rooms.delete(roomId); if (rooms.get(roomId)?.size === 0) rooms.delete(roomId); } function leaveAllRooms(ws) { for (const roomId of ws.rooms) leaveRoom(ws, roomId); } // On disconnect ws.on("close", () => leaveAllRooms(ws));
Message Routing — JSON Envelope
wss.on("connection", (ws, req) => { ws.userId = authenticate(req); ws.rooms = new Set(); ws.on("message", (raw) => { const msg = JSON.parse(raw.toString()); switch (msg.type) { case "join": joinRoom(ws, msg.room); broadcastRoom(msg.room, ws, { type: "presence", event: "join", userId: ws.userId, members: getRoomMembers(msg.room), }); break; case "leave": leaveRoom(ws, msg.room); broadcastRoom(msg.room, ws, { type: "presence", event: "leave", userId: ws.userId, }); break; case "message": broadcastRoom(msg.room, ws, { type: "message", room: msg.room, userId: ws.userId, text: msg.text, ts: Date.now(), }); break; } }); }); function getRoomMembers(roomId) { return [...(rooms.get(roomId) ?? [])].map((c) => c.userId); }
Room Metadata Pattern
// Rich room objects instead of bare Sets const rooms = new Map(); // roomId → { members: Set, meta: object } function createRoom(roomId, meta = {}) { rooms.set(roomId, { members: new Set(), meta, createdAt: Date.now() }); } function getRoomInfo(roomId) { const room = rooms.get(roomId); if (!room) return null; return { id: roomId, count: room.members.size, ...room.meta, }; }
Private / One-to-One Channel
// Canonical DM channel id: sort userIds so A→B and B→A resolve to same room function dmRoomId(a, b) { return [a, b].sort().join(":"); } function sendDM(fromId, toId, text) { const roomId = dmRoomId(fromId, toId); broadcastRoomAll(roomId, { type: "dm", from: fromId, text, ts: Date.now() }); }
Listing Rooms
function listRooms() { return [...rooms.entries()].map(([id, room]) => ({ id, count: typeof room === "object" && room.members ? room.members.size : room.size, })); }
Room Capacity Limits
const ROOM_CAPACITY = 100; function joinRoom(ws, roomId) { const room = getRoom(roomId); if (room.size >= ROOM_CAPACITY) { ws.send(JSON.stringify({ type: "error", code: "ROOM_FULL" })); return false; } room.add(ws); ws.rooms.add(roomId); return true; }
Full Room Reference
| Operation | Code |
|---|---|
| Create room | rooms.set(id, new Set()) |
| Join room | rooms.get(id).add(ws) |
| Leave room | rooms.get(id).delete(ws) |
| Delete empty room | if (!rooms.get(id).size) rooms.delete(id) |
| Count members | rooms.get(id)?.size ?? 0 |
| List members | [...rooms.get(id)] |
| Broadcast to room | for (ws of rooms.get(id)) ws.send(data) |
| Socket's rooms | ws.rooms (custom Set on the ws object) |
Gotchas
- In-memory rooms are per-process: two Node processes each have separate
roomsMaps. Use Redis pub/sub to synchronize rooms across processes (see Scaling). - Always delete empty rooms to prevent memory leaks in high-churn scenarios.
- Attach room membership to the
wsobject itself (ws.rooms) so you can clean up on disconnect without a full scan. - Path-based routing (one URL per room) is simpler than message-based join — use it when room assignment is static.
- Validate room names/IDs against an allowlist or length limit before using them as Map keys.