WebSocket

A protocol enabling full-duplex, real-time communication between client and server.

WebSocket is a communication protocol that provides full-duplex, bidirectional communication between a client and server over a single, long-lived TCP connection. Unlike HTTP where the client always initiates requests, WebSocket allows the server to push data to clients at any time.

HTTP vs WebSocket

AspectHTTPWebSocket
ConnectionNew connection per requestSingle persistent connection
DirectionClient → Server onlyBidirectional
OverheadHeaders sent every requestMinimal after handshake
Real-timePolling requiredNative push capability
Use caseRequest/response APIsReal-time updates

How WebSocket Works

1. Handshake: Client sends HTTP request with Upgrade: websocket header. Server responds with 101 (Switching Protocols). Connection upgrades from HTTP to WebSocket.

2. Data transfer: Both sides can send messages at any time. Messages are framed with minimal overhead (just a few bytes vs HTTP's hundreds).

3. Connection stays open: No need to reconnect for each message. The connection persists until either side closes it.

4. Heartbeats: Ping/pong frames keep connection alive and detect dead connections. If no pong received, connection is considered dead.

When to Use WebSocket

Real-time notifications: Chat messages, alerts, live updates. Server pushes new data instantly without client asking.

Live collaboration: Google Docs-style editing, Figma multiplayer, shared whiteboards. Changes sync immediately across users.

Live data feeds: Stock prices, sports scores, cryptocurrency tickers. Data changes frequently and must be fresh.

Gaming: Multiplayer games need instant communication. HTTP latency would make games unplayable.

IoT and telemetry: Sensors sending frequent readings. Opening a new HTTP connection for each reading is wasteful.

When NOT to Use WebSocket

Simple CRUD APIs: If your app just fetches and updates data occasionally, REST is simpler.

Infrequent updates: If data changes every few minutes, polling or SSE might be simpler than maintaining WebSocket connections.

One-way server push: If you only need server-to-client updates (no client messages), Server-Sent Events (SSE) is simpler.

Behind restrictive proxies: Some corporate proxies block WebSocket. Have an HTTP fallback ready.

WebSocket API Basics

Connection lifecycle:

  • onopen: Connection established
  • onmessage: Message received
  • onerror: Error occurred
  • onclose: Connection closed

Sending messages:

  • send(data): Send string or binary data
  • close(): Gracefully close connection

Ready states:

  • CONNECTING (0): Handshake in progress
  • OPEN (1): Ready to communicate
  • CLOSING (2): Closing handshake
  • CLOSED (3): Connection closed

Connection Management

Reconnection: Networks fail. Connections drop. Always implement automatic reconnection with exponential backoff:

  • First retry: 1 second
  • Second retry: 2 seconds
  • Third retry: 4 seconds
  • Max: 30 seconds

Heartbeats: Send periodic pings to detect dead connections. If the server doesn't respond, reconnect.

Graceful shutdown: Close connections properly when user navigates away. Send close frame, don't just abandon.

Common Patterns

Message types: Use a type field to distinguish messages: {type: "chat", payload: {text: "Hello"}} {type: "presence", payload: {user: "john", status: "online"}}

Request/response over WebSocket: Add request IDs to correlate responses: {id: "abc123", type: "getUser", payload: {userId: 1}} {id: "abc123", type: "userResponse", payload: {name: "John"}}

Rooms/channels: Subscribe to specific topics: {type: "subscribe", channel: "chat:general"}

Best Practices

Structure your messages: Use JSON with clear types. Don't send unstructured text.

Handle disconnection: Users don't notice when connections drop. Show connection status, queue messages, reconnect automatically.

Implement backpressure: If client is slow, buffer messages. If buffer is full, drop oldest or close connection.

Authenticate properly: Send auth token in first message or query param. Don't rely on cookies alone.

Monitor connections: Track open connections, message rates, errors. WebSocket issues are hard to debug without metrics.

Common Mistakes

1. No reconnection logic: Connection drops, app breaks, user refreshes. Always auto-reconnect.

2. Massive payloads: WebSocket isn't for file transfers. Send references (URLs) for large data.

3. Ignoring close codes: 1000 = normal close, 1006 = abnormal close, 1001 = going away. Different codes need different handling.

4. Not handling slow clients: Fast server, slow client = memory leak. Implement backpressure or drop slow clients.

Code Examples

WebSocket Client with Reconnection

class WebSocketClient {
  constructor(url) {
    this.url = url;
    this.reconnectDelay = 1000;
    this.maxReconnectDelay = 30000;
    this.connect();
  }

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

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

      // Authenticate
      this.send({ type: 'auth', token: getAuthToken() });
    };

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

    this.ws.onclose = (event) => {
      console.log(`Disconnected: ${event.code}`);
      if (event.code !== 1000) {
        this.scheduleReconnect();
      }
    };

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };
  }

  scheduleReconnect() {
    setTimeout(() => {
      console.log(`Reconnecting in ${this.reconnectDelay}ms...`);
      this.connect();
      // Exponential backoff
      this.reconnectDelay = Math.min(
        this.reconnectDelay * 2,
        this.maxReconnectDelay
      );
    }, this.reconnectDelay);
  }

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

  handleMessage(message) {
    switch (message.type) {
      case 'chat':
        displayChatMessage(message.payload);
        break;
      case 'notification':
        showNotification(message.payload);
        break;
    }
  }
}