🌙
☀️ Dark
PART 19

Real-Time Systems

WebSockets, SSE, pub/sub, idempotency.

Advanced 45 min read

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

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

▶ View Solution

Solution implementation.

Prerequisites

▶ 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.

javascript
▶ View Solution

Solution implementation.


// 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

javascript
▶ View Solution

Solution implementation.


// 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

javascript
▶ View Solution

Solution implementation.


// 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).

javascript
▶ View Solution

Solution implementation.

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
typescript
// 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.

🏠 Curriculum NextVolume 2