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

OperationCode
Create roomrooms.set(id, new Set())
Join roomrooms.get(id).add(ws)
Leave roomrooms.get(id).delete(ws)
Delete empty roomif (!rooms.get(id).size) rooms.delete(id)
Count membersrooms.get(id)?.size ?? 0
List members[...rooms.get(id)]
Broadcast to roomfor (ws of rooms.get(id)) ws.send(data)
Socket's roomsws.rooms (custom Set on the ws object)

Gotchas

  • In-memory rooms are per-process: two Node processes each have separate rooms Maps. 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 ws object 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.