Networking

WebSocket

Persistent bidirectional connection — real-time over a single TCP socket

WebSocket is a protocol that upgrades an HTTP connection into a long-lived bidirectional channel — server and client can each send messages whenever they want, instead of HTTP's strict request/response. Used for live chat, multiplayer games, real-time dashboards, and collaborative editors. One handshake, then unlimited message exchange in both directions.

  • HandshakeHTTP Upgrade — single 101 Switching Protocols response
  • DirectionBidirectional — both sides can initiate messages
  • Default ports80 (ws://), 443 (wss://) — same as HTTP
  • Frame overhead2-14 bytes per message (vs HTTP's hundreds)
  • Default stateStateful — connection survives multiple messages
  • Used byChat, live dashboards, multiplayer games, collaborative editors

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

How a WebSocket connection is established

The connection starts as an ordinary HTTP/1.1 request with two special headers:

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

The server, if it supports WebSocket on this path, responds with:

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

The Sec-WebSocket-Accept is the SHA-1 hash of the client's Sec-WebSocket-Key concatenated with a fixed string, base64-encoded. Crude but effective: it confirms the server actually understood the WebSocket protocol (rather than echoing back the key naively).

From that point forward, the same TCP connection switches from HTTP framing to WebSocket framing. Both sides can send messages whenever they want.

WebSocket vs alternatives

WebSocketHTTP pollingHTTP long-pollingServer-Sent EventsHTTP/2 push
DirectionBidirectionalClient → ServerClient → ServerServer → Client onlyServer → Client (mostly removed)
Latency (server→client)Sub-millisecondPolling interval (1s+)Sub-secondSub-millisecondSub-millisecond
Server resources per client1 connection1 conn per poll1 long-held conn1 long-held connMultiplexed on shared conn
ReconnectionManualTrivial (next poll)ManualAuto (built-in)Built-in
Browser supportExcellentUniversalUniversalExcellent (no IE)Mostly removed
Best forChat, games, collaborationSimple "any updates?" checksNotifications when polling is too chattyOne-way feeds (news, stock prices)Performance-only optimization

For a chat app, polling every second wastes bandwidth and adds 500ms average latency. WebSocket has near-zero latency and zero overhead per message. The trade-off is connection management — handling reconnects, scaling state across servers, dealing with idle connection timeouts.

When to use WebSocket

  • Real-time chat and messaging. Slack, Discord, WhatsApp Web all use WebSocket (or socket.io, which falls back when needed). Sub-100ms message delivery is impossible with HTTP polling.
  • Multiplayer games. Player position updates dozens of times per second from each player to all others. WebSocket's per-message overhead is the right cost for this rate.
  • Collaborative editors. Google Docs, Figma, Notion — every keystroke from every user broadcasts to every other user. WebSocket plus operational transformation or CRDTs.
  • Live dashboards. Trading prices, system metrics, score boards. Server pushes updates as they happen.
  • IoT control. Bidirectional device-to-cloud communication where the cloud needs to send commands. MQTT is more common for IoT but WebSocket also works.

JavaScript: WebSocket client

const ws = new WebSocket('wss://example.com/chat');

ws.addEventListener('open', () => {
  console.log('connected');
  ws.send(JSON.stringify({ type: 'subscribe', channel: 'general' }));
});

ws.addEventListener('message', (e) => {
  const msg = JSON.parse(e.data);
  console.log('received:', msg);
});

ws.addEventListener('close', (e) => {
  console.log('closed', e.code, e.reason);
  // Reconnect with exponential backoff
  setTimeout(reconnect, getBackoff());
});

ws.addEventListener('error', (e) => {
  console.error('socket error', e);
});

// Send a message later
function sendChat(text) {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: 'chat', text }));
  } else {
    // Queue for after reconnect
    pendingMessages.push({ type: 'chat', text });
  }
}

Node.js: minimal echo WebSocket server

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (socket, req) => {
  console.log('client connected from', req.socket.remoteAddress);

  socket.on('message', (data) => {
    // Echo back to sender
    socket.send(data);

    // Broadcast to everyone else
    wss.clients.forEach((c) => {
      if (c !== socket && c.readyState === socket.OPEN) c.send(data);
    });
  });

  socket.on('close', () => console.log('client disconnected'));

  // Heartbeat to detect dead connections
  socket.isAlive = true;
  socket.on('pong', () => { socket.isAlive = true; });
});

// Periodically check for dead connections
setInterval(() => {
  wss.clients.forEach((s) => {
    if (!s.isAlive) return s.terminate();
    s.isAlive = false;
    s.ping();
  });
}, 30000);

Python: websockets library

import asyncio
import websockets

async def echo(websocket):
    async for message in websocket:
        await websocket.send(message)  # echo

async def main():
    async with websockets.serve(echo, 'localhost', 8080):
        await asyncio.Future()  # run forever

asyncio.run(main())

Reconnection with exponential backoff

WebSocket has no built-in reconnect. Implement it with exponential backoff and jitter:

class ReconnectingWebSocket {
  constructor(url) {
    this.url = url;
    this.attempts = 0;
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);
    this.ws.addEventListener('open', () => { this.attempts = 0; });
    this.ws.addEventListener('close', () => this.scheduleReconnect());
    this.ws.addEventListener('error', () => this.ws.close());
  }

  scheduleReconnect() {
    const base = Math.min(1000 * 2 ** this.attempts, 30000); // cap at 30s
    const jitter = base * (0.9 + Math.random() * 0.2);        // ±10%
    this.attempts++;
    setTimeout(() => this.connect(), jitter);
  }

  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) this.ws.send(data);
    // else: queue for after reconnect, application-specific
  }
}

Without jitter, all clients reconnect at the same instant after a server crash — a "thundering herd" that can DDoS your own service. The random ±10% spreads reconnect attempts across a window.

Scaling WebSocket servers

Each WebSocket holds an open TCP connection. Servers can typically handle 10K-100K concurrent connections per machine (kernel and tunable limit on file descriptors).

To scale beyond that, you need a horizontal-scaling pattern:

  1. Pub/Sub backplane. Use Redis Pub/Sub, NATS, or Kafka. When server A receives a message that should go to a client on server B, A publishes to a channel; B's subscription delivers it to its connected client.
  2. Sticky sessions. Load balancers route a client's reconnects to the same server. Avoids re-establishing per-server state.
  3. Connection state externalized. Don't keep "user X is in channel Y" only in process memory; store it in Redis or similar so any server can answer.
  4. Health checks. Old connections to dead servers must time out cleanly. TCP keepalive + WebSocket ping/pong every 30s.

Common WebSocket issues

  • Idle connection timeouts. NAT devices, load balancers, and proxies often time out connections after ~5 minutes of inactivity. Send WebSocket ping frames every 30 seconds to keep the connection alive.
  • Authentication after upgrade. The HTTP upgrade request can carry cookies or headers, but once the connection is open, those don't apply to messages. Validate auth tokens in your message handlers if authorization changes during the session.
  • Backpressure when one client is slow. If a slow client can't drain messages fast enough, your send buffer fills up and the server runs out of memory. Apply per-client send queues with size limits; close clients that fall too far behind.
  • Message ordering across reconnects. After a reconnect, missed messages are gone unless the server kept them. Use sequence numbers and let the client request "everything since seq N" on reconnect.
  • Mixed-content blocks. An HTTPS page can't open a ws:// WebSocket — only wss://. Always use TLS in production.
  • JSON parsing in hot path. Every message goes through JSON.parse. For high-throughput streams (60+ msgs/sec per client), consider binary framing (MessagePack, Protobuf) — 2-3× faster parsing and smaller payloads.

Frequently asked questions

When should I use WebSocket over plain HTTP?

When you need server-initiated messages (server pushes data without the client polling), bidirectional communication, or low-latency for many small messages. Chat apps, live trading dashboards, multiplayer games, collaborative editors. For one-off requests or strictly request-response patterns, HTTP/2 is simpler — no connection management.

What's the difference between WebSocket and Server-Sent Events?

WebSocket is bidirectional — both sides send. SSE (Server-Sent Events) is server-to-client only over an open HTTP connection. SSE is much simpler — it's just a long-running HTTP/1.1 GET that streams text events — but you need a separate HTTP request for client-to-server. SSE auto-reconnects on disconnect; WebSocket doesn't natively. Use SSE if you only need server pushes (notifications, news feed). Use WebSocket if you also need client-initiated messages.

Why does WebSocket start as HTTP?

To work with existing infrastructure. Firewalls, load balancers, and proxies are designed around HTTP traffic on ports 80/443. Starting as HTTP and upgrading lets WebSockets traverse most networks without firewall reconfiguration. The Upgrade handshake is the same TCP connection; the protocol just switches from HTTP to WebSocket framing after the 101 response.

How does the WebSocket framing work?

Each message becomes a frame with a 2-14 byte header (opcode, length, masking key) followed by the payload. The header overhead is tiny compared to HTTP's hundreds-of-bytes-per-request. Messages can be split across frames (large file transfers) and reassembled. Control frames (ping, pong, close) coordinate connection state.

How do you scale WebSocket servers?

Each WebSocket holds an open TCP connection — one per client. A single server typically handles 10K-100K concurrent connections. To scale beyond, use a Pub/Sub backend (Redis, NATS, Kafka) so any server can deliver messages to any client. Sticky sessions at the load balancer route a client's reconnects to the same server. SaaS solutions (Pusher, Ably, AWS API Gateway WebSocket) handle the scaling for you.

What about connection failures and reconnection?

WebSocket has no built-in reconnection — applications must implement it. The pattern is exponential backoff with jitter (1s, 2s, 4s, 8s, 16s capped at 30s, plus random ±10% to avoid thundering herd). Some libraries (socket.io) wrap this. After reconnecting, the application typically re-establishes session state (re-subscribe to channels, fetch missed messages).

Is WebSocket secure?

WebSocket over TLS (wss://) is encrypted exactly like HTTPS — same TLS handshake, same security guarantees. Plain ws:// is unencrypted; never use it on the public internet. Beyond transport security, you also need application-level auth (JWT tokens, session cookies) since WebSocket has no built-in auth scheme. Validate tokens on every message if state matters.