100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Web Development

Building a Real-Time Chat App

A hands-on walkthrough of designing a WebSocket-based chat application, from connection lifecycle and message schema to rooms, presence, and delivery guarantees.

PracticeIntermediate11 min readJul 10, 2026
Analogies

Building a Real-Time Chat App

A real-time chat app built on WebSockets needs three core pieces: a connection manager that tracks which sockets belong to which users, a message protocol that defines the shape of every event flowing over the wire, and a routing layer that decides which connected sockets should receive each message. Getting these three right early avoids painful rewrites later, especially once you add rooms, typing indicators, and delivery receipts.

🏏

Cricket analogy: The connection manager is like a scorer's roster tracking exactly which players are on the field right now, so an update ('four runs!') gets relayed only to the right broadcast booth and not to players who've already walked off.

Designing the Message Protocol

Every message sent over the socket should follow a consistent envelope, typically a JSON object with a type field discriminating the event (e.g. message, typing, presence, read_receipt) and a payload field carrying the event-specific data. This lets both client and server dispatch on type with a simple switch statement, and makes it easy to add new event types later without breaking existing handlers, since unknown types can simply be ignored by older clients.

🏏

Cricket analogy: A typed message envelope is like a scorecard entry format: every ball has a consistent structure (over, bowler, outcome) so the scoring app can process 'wicket', 'four', and 'wide' events with the same parser logic.

Rooms, Presence, and Fan-out

Chat rooms are typically modeled server-side as a mapping from a room ID to a set of connected socket references; when a message arrives for a room, the server iterates that set and writes the message to each socket, a pattern called fan-out. Presence (who's online, who's typing) is usually implemented as ephemeral state kept in memory or a fast store like Redis, published to room members whenever a user connects, disconnects, or changes status, since presence data doesn't need durable storage the way messages do.

🏏

Cricket analogy: Fan-out to a room is like a stadium's PA announcer reading out a boundary to every stand simultaneously, while presence tracking is like the scoreboard operator updating 'players on field' live as substitutes come and go, without keeping that as permanent match history.

Server-Side Room Broadcast Example

javascript
// Minimal room-based fan-out with the 'ws' library (Node.js)
const rooms = new Map(); // roomId -> Set<WebSocket>

function joinRoom(ws, roomId, userId) {
  if (!rooms.has(roomId)) rooms.set(roomId, new Set());
  rooms.get(roomId).add(ws);
  ws.roomId = roomId;
  ws.userId = userId;
  broadcast(roomId, { type: 'presence', payload: { userId, status: 'online' } }, ws);
}

function broadcast(roomId, message, exclude) {
  const sockets = rooms.get(roomId);
  if (!sockets) return;
  const data = JSON.stringify(message);
  for (const client of sockets) {
    if (client !== exclude && client.readyState === client.OPEN) {
      client.send(data);
    }
  }
}

wss.on('connection', (ws) => {
  ws.on('message', (raw) => {
    const msg = JSON.parse(raw);
    if (msg.type === 'join') joinRoom(ws, msg.payload.roomId, msg.payload.userId);
    if (msg.type === 'message') broadcast(ws.roomId, { type: 'message', payload: msg.payload });
  });

  ws.on('close', () => {
    rooms.get(ws.roomId)?.delete(ws);
    broadcast(ws.roomId, { type: 'presence', payload: { userId: ws.userId, status: 'offline' } });
  });
});

For a single-server deployment, an in-memory Map of rooms works fine. Once you scale to multiple server instances, you need a shared pub/sub layer (Redis Pub/Sub, NATS, or a managed service) so a message sent to a socket connected to server A can be fanned out to sockets connected to server B.

Don't rely on ws.send() succeeding as proof of delivery. TCP buffering means send() can return before the client has actually received or processed the message. For guaranteed delivery (e.g. 'message read' semantics), implement application-level acknowledgments where the client explicitly confirms receipt.

  • Track connections with a structure mapping room IDs to sets of live sockets, and clean up on disconnect to avoid memory leaks.
  • Use a consistent typed message envelope ({ type, payload }) so client and server can dispatch on event type.
  • Presence and typing indicators are ephemeral state, best kept in memory or Redis, not the durable message store.
  • Fan-out means iterating the room's socket set and writing to each one; check readyState before sending.
  • Scale beyond one server with a pub/sub layer like Redis so messages reach sockets connected to other instances.
  • send() returning does not guarantee delivery; use application-level ack messages for read receipts.
  • Persist chat messages to a database independently of the real-time layer so history survives reconnects and server restarts.

Practice what you learned

Was this page helpful?

Topics covered

#WebDevelopment#WebSocketsStudyNotes#BuildingARealTimeChatApp#Building#Real#Time#Chat#StudyNotes#SkillVeris#ExamPrep