Real-Time Systems
WebSockets, SSE, pub/sub, idempotency.
PART 19 — REAL-TIME SYSTEMS
// Server with Heartbeats
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
});
setInterval(() => {
wss.clients.forEach((ws) => {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
Engineering Challenge
Write a system that guarantees exact-once processing of chat messages even if the client disconnects immediately after sending. Use a correlation ID (UUID generated on the client) and a Redis set to deduplicate incoming messages on the server.
View Solution Approach
Client sends: `{ id: 'uuid-123', text: 'hello' }`. Server receives, checks Redis `SISMEMBER processed_messages uuid-123`. If true, ignore (duplicate). If false, save to DB, add to Redis, broadcast to others, and send ACK back to client.
Revision Sheet
- Long Polling: HTTP request held open until data is ready.
- SSE: Unidirectional HTTP stream from server.
- WebSockets: Bidirectional, persistent TCP connection.
- Heartbeats: Ping/pong to detect dead connections.
- Pub/Sub: Redis layer required to route messages across multiple WebSocket servers.
Connections
Real-time systems rely heavily on the networking concepts discussed in PART 3, and to scale them, you will use the caching and distributed systems patterns discussed in PART 21 and PART 22.
Learning Objectives
- Understand the transition from traditional request-response to event-driven, bidirectional communication.
- Compare and implement Long Polling, Server-Sent Events (SSE), and WebSockets.
- Master the connection lifecycle, heartbeats, and reconnection logic.
- Understand presence, pub/sub architectures, and message ordering in distributed real-time systems.
- Handle edge cases such as duplicate messages, idempotency, and scaling stateful connections.
▶ View Solution
Solution implementation.
Prerequisites
- Solid understanding of HTTP/1.1 and REST APIs.
- Knowledge of the TCP/IP stack and socket basics.
- Familiarity with Node.js async patterns and Express/Fastify.
- Understanding of horizontal scaling and load balancers.
▶ View Solution
Solution implementation.
Why Does This Exist?
The web was originally built for a stateless, unidirectional request-response paradigm: the client asks for a document, and the server responds. But modern applications—chat apps, collaborative document editors, live stock tickers, and multiplayer games—require data to flow instantly in both directions. Real-time systems exist to break the HTTP request-response limitation, allowing servers to push data to clients the moment it happens, without waiting for the client to ask.
▶ View Solution
Solution implementation.
The Problem Before the Solution
Before real-time protocols, if a user wanted to know if they received a new message, the client had to constantly ask the server: "Any new messages? Any new messages?" This is known as Short Polling.
// Short Polling Example
setInterval(async () => {
const messages = await fetch('/api/messages');
render(messages);
}, 1000);
▶ View Solution
Solution implementation.
▶ View Solution
Solution implementation.
Why the Old Approach Breaks
Short polling is incredibly wasteful. If you have 10,000 users polling every second, that's 10,000 HTTP requests per second hitting your servers. 99% of these requests will return empty data because nothing has changed. It wastes bandwidth, CPU, and database resources parsing and authenticating useless requests. It also introduces latency: if an event happens immediately after a poll, the user won't see it until the next interval.
▶ View Solution
Solution implementation.
History
To solve the polling nightmare, engineers invented Long Polling. The client makes an HTTP request, but the server deliberately holds the connection open until it has data to send, then responds. Later, HTML5 introduced Server-Sent Events (SSE) for unidirectional server-to-client streaming over HTTP. Finally, WebSockets were standardized as a fully bidirectional, persistent protocol running over a single TCP connection.
▶ View Solution
Solution implementation.
Mental Model (Analogy -> Reality)
Short Polling: Calling your friend every 5 minutes to ask if their flight landed.
Long Polling: Calling your friend, and they don't say anything until their flight lands, then they hang up. You immediately call them back for the next update.
Server-Sent Events: Your friend gives you a radio, and whenever something happens, they broadcast it to you. You can only listen.
WebSockets: You and your friend are on an open phone call. You can both talk and listen at any time without having to dial again.
▶ View Solution
Solution implementation.
Internal Working
A WebSocket starts as a standard HTTP GET request with an Upgrade: websocket header. If the server agrees, it responds with an HTTP/1.1 101 Switching Protocols status. The TCP connection is kept alive, and both parties can now send binary or text frames asynchronously. Because the connection is persistent, the server must keep a file descriptor open in memory for every connected client. This is fundamentally different from stateless HTTP scaling.
▶ View Solution
Solution implementation.
Visual Explanation
WebSocket Handshake:
Client Server
| ----- HTTP GET / Upgrade: websocket ------> |
| <---- HTTP/1.1 101 Switching Protocols ---- |
| |
| ============== OPEN TCP =================== |
| <----------- Data Frame (Push) ------------ |
| ------------ Data Frame (Send) -----------> |
▶ View Solution
Solution implementation.
Syntax
// Client-side WebSocket
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'hello' }));
};
ws.onmessage = (event) => {
console.log('Received:', event.data);
};
▶ View Solution
Solution implementation.
▶ View Solution
Solution implementation.
Tiny Example
// Node.js Server (using 'ws' library)
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', (data) => {
console.log('received: %s', data);
ws.send(`Server echoes: ${data}`);
});
});
▶ View Solution
Solution implementation.
▶ View Solution
Solution implementation.
Walkthrough
In the tiny example, we spin up a server listening on port 8080. When a client connects, the 'connection' event fires, yielding a ws object representing that specific client's TCP socket. We attach an event listener for 'message' on that socket. When data arrives, we read it and use ws.send() to push data back. Notice there are no routes, no headers after the handshake, just raw event-driven messaging.
▶ View Solution
Solution implementation.
Break It
What happens if a user connects, their laptop loses Wi-Fi, but the TCP connection isn't cleanly closed? The server still thinks the connection is open, holding memory hostage. If this happens to 100,000 users, you get a memory leak and your server crashes.
▶ View Solution
Solution implementation.
Debug It
To fix ghost connections, we must implement a Heartbeat (Ping/Pong) mechanism. The server periodically sends a small 'ping' frame. If the client doesn't reply with a 'pong' within a timeout, the server forcefully terminates the socket and clears the memory.
▶ View Solution
Solution implementation.
Mini Project (20-30 min)
Implement a chat room with connection lifecycle management (connect, heartbeat, disconnect cleanup). Track which users are currently online (Presence).
Bigger Project (1-2 hours)
Build a simple chat room using Socket.io and Express, broadcasting messages to all connected clients in real-time.
▶ View Solution
// Implementation for Real-Time Systems
console.log("Bigger project solution");
Interview Questions
Easy: What is the difference between WebSockets and Server-Sent Events?
WebSockets are fully bidirectional (client to server and server to client). SSE is unidirectional (server pushing to client only) and runs over standard HTTP.
Medium: How does a WebSocket bypass the CORS policy?
WebSockets do not strictly enforce CORS like XMLHttpRequest/fetch. The browser sends an `Origin` header during the HTTP upgrade request, and it is entirely up to the server to check this header and reject the handshake if the origin is untrusted.
Hard: How do you guarantee message ordering in a distributed WebSocket + Pub/Sub architecture?
WebSockets guarantee ordered delivery over the single TCP connection. However, across a distributed pub/sub, race conditions can occur. To guarantee global ordering, messages must be sequenced (e.g., given an incrementing ID) at a single source of truth (like a database or a single Redis stream) before being fanned out. The client buffers and reorders based on these sequence IDs.
Senior: Describe how to design the architecture for WhatsApp's real-time messaging system.
Requires deep dive into persistent connections (often using Erlang/Elixir or C++ rather than Node.js for massive scale), load balancer TCP proxying, pub/sub for cross-node routing, offline message queues, idempotency keys for retries, and end-to-end encryption key exchanges.