Skip to content

WebSockets

WebSockets provide a persistent, full-duplex communication channel over a single TCP connection. After the initial HTTP handshake, either side can send messages at any time — no request-response cycle required.


The Problem WebSockets Solve

HTTP Polling (inefficient):
─────────────────────────
Client: "Any new messages?"  → Server: "No"
Client: "Any new messages?"  → Server: "No"
Client: "Any new messages?"  → Server: "No"
Client: "Any new messages?"  → Server: "Yes! Here they are"
(Wasted requests, high latency)

Long Polling (better, but still heavy):
────────────────────────────────────────
Client: "Give me new messages (wait up to 30s)" → Server: [holds connection]
                                                   Server: "Here's a message!" (30s later)
Client: "Give me new messages (wait up to 30s)" → Server: [holds connection]
...
(Better, but still HTTP overhead per cycle)

WebSocket (ideal for real-time):
─────────────────────────────────
Client ←──── persistent TCP connection ────▶ Server
              (both send whenever they want)

The WebSocket Handshake

WebSockets start as HTTP and then "upgrade" to the WebSocket protocol.

Client → Server (HTTP Upgrade Request):
────────────────────────────────────────
GET /chat HTTP/1.1
Host: api.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==  ← random base64 nonce
Sec-WebSocket-Version: 13
Origin: https://myapp.com

Server → Client (101 Switching Protocols):
──────────────────────────────────────────
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=  ← derived from nonce

──── HTTP ends here. WebSocket begins. ────

Now both sides can send frames freely:
Client → Server: { "type": "message", "text": "Hello!" }
Server → Client: { "type": "message", "text": "Hi back!" }
Server → Client: { "type": "presence", "user": "Madhu", "status": "online" }

WebSocket Architecture

mindmap
  root((WebSockets))
    Handshake
      HTTP Upgrade
      101 Switching Protocols
      Sec-WebSocket-Key
      Sec-WebSocket-Accept
    Message Types Frames
      Text frame UTF-8
      Binary frame
      Ping frame keepalive
      Pong frame response to ping
      Close frame
    Features
      Full-duplex
      Persistent connection
      Low latency
      Low overhead per message
    Challenges
      Scaling
        Sticky sessions
        Pub/Sub Redis
        Horizontal scaling hard
      State management
        Connection state on server
      Reconnection
        Exponential backoff
        Resume state
      Load balancers
        Need WS support
        Nginx proxy_pass upgrade
    Use Cases
      Chat applications
      Live notifications
      Collaborative editing
      Real-time dashboards
      Multiplayer games
      Financial tickers
      Live sports scores

Browser WebSocket API

// Connect
const ws = new WebSocket('wss://api.example.com/ws');
// wss:// = WebSocket Secure (like https:// for WS)
// ws://  = unencrypted (avoid in production)

// Connection opened
ws.addEventListener('open', () => {
  console.log('Connected!');
  ws.send(JSON.stringify({ type: 'subscribe', channel: 'chat' }));
});

// Receive message
ws.addEventListener('message', (event) => {
  const data = JSON.parse(event.data);
  console.log('Received:', data);
});

// Connection closed
ws.addEventListener('close', (event) => {
  console.log('Closed:', event.code, event.reason);
  // Reconnect logic here
});

// Error
ws.addEventListener('error', (error) => {
  console.error('WebSocket error:', error);
});

// Send a message
ws.send(JSON.stringify({ type: 'message', text: 'Hello!' }));

// Close gracefully
ws.close(1000, 'User disconnected');

WebSocket Close Codes

Code Meaning
1000 Normal closure
1001 Going away (navigated away, server restart)
1002 Protocol error
1003 Unsupported data type
1006 Abnormal closure (no close frame received)
1007 Invalid data (e.g., non-UTF-8 in text frame)
1008 Policy violation
1009 Message too large
1011 Server internal error

Reconnection with Exponential Backoff

class ReconnectingWebSocket {
  constructor(url) {
    this.url = url;
    this.reconnectDelay = 1000;  // start at 1 second
    this.maxDelay = 30000;       // max 30 seconds
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log('Connected');
      this.reconnectDelay = 1000;  // reset on success
    };

    this.ws.onclose = () => {
      console.log(`Reconnecting in ${this.reconnectDelay}ms...`);
      setTimeout(() => this.connect(), this.reconnectDelay);
      // Exponential backoff with jitter
      this.reconnectDelay = Math.min(
        this.reconnectDelay * 2 + Math.random() * 1000,
        this.maxDelay
      );
    };

    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      this.handleMessage(data);
    };
  }

  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    }
  }
}

Jitter (the Math.random() * 1000) prevents all clients from reconnecting simultaneously after a server restart — also called the "thundering herd" problem.


Scaling WebSockets

The hardest part of WebSockets in production. Each connection is stateful and lives on one server.

The Problem

Load Balancer
      │
   ┌──┴──┐
   ▼     ▼
Server1  Server2
 [userA] [userB]

userA sends a message to userB.
Server1 has no connection to userB → can't deliver!

Solution: Pub/Sub with Redis

Load Balancer
      │
   ┌──┴──┐
   ▼     ▼
Server1  Server2
 [userA] [userB]
   │         │
   └────┬────┘
        ▼
      Redis
    (Pub/Sub)

userA sends message to userB:
  Server1 → publish to Redis channel "user:B"
  Server2 is subscribed → receives → sends to userB's WebSocket ✅

Nginx Config for WebSocket Proxy

server {
  listen 443 ssl;

  location /ws {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;       # Required for WS
    proxy_set_header Connection "upgrade";         # Required for WS
    proxy_set_header Host $host;
    proxy_read_timeout 3600s;                      # Keep alive for 1 hour
  }
}

Message Protocol Design

WebSockets are just a pipe. You design the message format on top.

// Common pattern: type + payload
{
  "type": "chat.message",
  "payload": {
    "channelId": "general",
    "text": "Hello everyone!",
    "userId": "42"
  },
  "id": "msg-uuid-here",     // For ack tracking
  "timestamp": 1704067200
}

// Server acknowledgment
{
  "type": "ack",
  "id": "msg-uuid-here",
  "status": "delivered"
}

// Heartbeat / keepalive
{
  "type": "ping"
}
{
  "type": "pong"
}

// Error
{
  "type": "error",
  "code": "UNAUTHORIZED",
  "message": "Token expired"
}

WebSocket vs SSE vs Polling

┌──────────────────┬───────────────┬───────────────┬────────────────┐
│                  │ WebSocket     │ SSE           │ Long Polling   │
├──────────────────┼───────────────┼───────────────┼────────────────┤
│ Direction        │ Full-duplex   │ Server → only │ Server → only  │
│ Protocol         │ WS (over TCP) │ HTTP          │ HTTP           │
│ Browser support  │ All modern    │ All modern    │ All            │
│ Reconnect        │ Manual        │ Auto          │ Manual         │
│ Overhead         │ Low per msg   │ Low           │ High per cycle │
│ Load balancing   │ Hard (sticky) │ Easier        │ Easy           │
│ HTTP/2 compat    │ Separate conn │ Multiplexed   │ Multiplexed    │
│ Best for         │ Chat, games   │ Notifications │ Fallback       │
└──────────────────┴───────────────┴───────────────┴────────────────┘

WebSocket readyState Values

WebSocket.CONNECTING  // 0 — Handshaking
WebSocket.OPEN        // 1 — Connected and ready
WebSocket.CLOSING     // 2 — Close handshake in progress
WebSocket.CLOSED      // 3 — Connection closed

// Check before sending:
if (ws.readyState === WebSocket.OPEN) {
  ws.send(message);
}

Interview tips: - Explain the HTTP → WS upgrade handshake (101 Switching Protocols) - Full-duplex = both sides send without waiting — contrast with SSE (server-only) - Scaling is the hardest part — Redis pub/sub is the standard answer - Exponential backoff + jitter for reconnection (thundering herd) - WebSockets are stateful — this makes horizontal scaling complex