Real-Time & Communication
WebSockets

WebSockets

The Problem

HTTP is request-response: the client asks, the server answers. But what if the server needs to tell the client something the client didn't ask for?

Chat app example:
  User A sends a message → User B should see it instantly

With HTTP:
  User B would need to ask: "Any new messages?"
  Every 1 second... every 2 seconds...
  → Wasteful, slow, doesn't scale

The Mental Model

Think of HTTP as a phone call where only one person can speak at a time. Every time you want to talk, you dial, say your piece, and hang up.

WebSockets are like a walkie-talkie. Both sides keep the channel open and can speak whenever they want.

HTTP:       Client → "Hello?" → Server → "Hi!" → Hang up
            Client → "Any news?" → Server → "No" → Hang up
            Client → "Any news?" → Server → "Yes, here!" → Hang up

WebSocket:  Client ←→ Open Channel ←→ Server
            Server: "Hey, new message!" (no client asking)
            Client: "Got it!" (instant)
            Server: "And another one!" (instant)

How WebSockets Work

Step 1: HTTP Upgrade Handshake

A WebSocket connection starts as a regular HTTP request, then "upgrades" to a WebSocket:

Client → Server:
  GET /chat HTTP/1.1
  Host: example.com
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
  Sec-WebSocket-Version: 13

Server → Client:
  HTTP/1.1 101 Switching Protocols
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Connection is now a WebSocket. Both sides can send messages at any time.

Step 2: Full-Duplex Communication

After the handshake:

  Client ←→ WebSocket ←→ Server

  Both sides can send messages independently.
  No more request-response pattern.
  Messages are small (2-14 bytes header vs HTTP's hundreds of bytes).

WebSocket Frame Format

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -+
|                     Payload Data (continued)                  |
+---------------------------------------------------------------+

Key points:

  • FIN bit: Indicates this is the final fragment of a message
  • Opcode: Tells the type (0x1 = text, 0x2 = binary, 0x8 = close, 0x9 = ping, 0xA = pong)
  • Mask: Client-to-server messages MUST be masked (security)
  • Payload: The actual data

WebSocket Implementation (Node.js)

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
 
wss.on('connection', (ws) => {
  console.log('New client connected');
  
  // Send welcome message
  ws.send(JSON.stringify({ type: 'welcome', message: 'Connected!' }));
  
  // Handle incoming messages
  ws.on('message', (data) => {
    const message = JSON.parse(data);
    
    // Broadcast to all connected clients
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify({
          type: 'chat',
          user: message.user,
          text: message.text,
          timestamp: Date.now()
        }));
      }
    });
  });
  
  // Handle disconnection
  ws.on('close', () => {
    console.log('Client disconnected');
  });
  
  // Handle errors
  ws.on('error', (error) => {
    console.error('WebSocket error:', error);
  });
});

WebSocket in Python

import asyncio
import websockets
 
connected_clients = set()
 
async def handler(websocket, path):
    connected_clients.add(websocket)
    try:
        async for message in websocket:
            # Broadcast to all connected clients
            for client in connected_clients:
                if client != websocket:
                    await client.send(message)
    finally:
        connected_clients.remove(websocket)
 
async def main():
    async with websockets.serve(handler, "localhost", 8070):
        await asyncio.Future()  # Run forever
 
asyncio.run(main())

WebSocket Scaling

The Challenge: WebSocket connections are stateful and persistent. You can't just round-robin them across servers.

Problem:
  User A → Server 1 (WebSocket)
  User B → Server 2 (WebSocket)
  
  User A sends message to User B
  Server 1 doesn't know User B is on Server 2!

Solution: Pub/Sub with Redis

User A → Server 1 → Redis Pub/Sub → Server 2 → User B

1. Each WebSocket server subscribes to a Redis channel
2. When a message arrives, publish to Redis
3. Redis fans out to all subscribers
4. Servers deliver to relevant connected clients
import redis
 
r = redis.Redis()
 
class WebSocketServer:
    def __init__(self):
        self.pubsub = r.pubsub()
        self.pubsub.subscribe('chat_messages')
    
    def handle_message(self, user_id, message):
        # Publish to Redis
        r.publish('chat_messages', json.dumps({
            'user_id': user_id,
            'message': message,
            'server_id': self.server_id
        }))
    
    def listen_for_messages(self):
        for item in self.pubsub.listen():
            if item['type'] == 'message':
                data = json.loads(item['data'])
                # Deliver to connected clients on this server
                self.deliver_to_local_clients(data)

WebSocket Properties

PropertyValue
ConnectionPersistent, full-duplex
LatencyVery low (~1ms overhead)
Server pushNative support
Binary dataSupported
ReconnectionManual (must implement)
HTTP cachingNot available
Browser supportAll modern browsers
Max connections~65K per server (file descriptor limit)

When to Use WebSockets

  • Chat applications: Slack, WhatsApp Web, Discord
  • Real-time games: Multiplayer online games
  • Live dashboards: Stock tickers, monitoring dashboards
  • Collaborative editing: Google Docs, Figma
  • Live notifications: Social media feeds, breaking news