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
// 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
readyStatebefore 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
1. What is 'fan-out' in the context of a chat room?
2. Why is a typed `{ type, payload }` message envelope recommended?
3. Why is presence data usually kept in memory or Redis rather than a relational database?
4. What problem does a Redis Pub/Sub layer solve for a chat app running on multiple server instances?
5. Why shouldn't `ws.send()` returning without error be treated as proof of message delivery?
Was this page helpful?
You May Also Like
WebSockets vs WebRTC
A practical comparison of WebSockets and WebRTC, covering their transport models, connection setup, and when to reach for each in a real-time application.
Debugging WebSocket Connections
Practical techniques for diagnosing failed handshakes, dropped connections, and mysterious silent disconnects in WebSocket-based applications.
WebSockets Quick Reference
A condensed reference covering the WebSocket API, handshake headers, close codes, and common patterns for fast lookup while building or reviewing real-time features.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics
Web DevelopmentNext.js Study Notes
JavaScript · 30 topics