CodeNotes
Back to library
system-designJune 2, 20266 min read

WebSockets Protocol

Understand the difference between HTTP and WebSockets, the handshake mechanism, and a step-by-step real-time server setup guide.

WebSockets Protocol

Why WebSockets are used

The standard HTTP protocol is unidirectional. This means:

  1. The Client must start the conversation by sending a request (e.g. "Give me new messages").
  2. The Server responds with the data and then immediately closes the connection.

If you are building real-time applications like a multiplayer game, a stock market chart, or a chat app, HTTP fails. If someone sends you a chat message, the server has no way to push it to your browser because the server cannot initiate communication.

The Old Hack: Polling

Before WebSockets, developers used Polling:

  • Short Polling: The browser requests /new-messages every 2 seconds. This wastes bandwidth and server resources, as 99% of requests return empty lists.
  • Long Polling: The browser requests /new-messages, and the server holds the request open until a message arrives, then responds and closes. The browser immediately starts a new request. This is complex to manage and scale.
HTTP Polling:
Client ===(New message?)===> Server ===(No)===> Client (Repeat every 2s)

WebSockets were created to solve this by providing a permanent, bidirectional bridge.


Mental Model

Let's compare HTTP and WebSockets using communication analogies:

  • HTTP (Mail Delivery): You write a letter (Request), drop it in the mailbox, and wait. The mail carrier delivers it to the server. The server reads it, writes a response letter, sends it back, and the transaction is complete. If you want to ask another question, you must write a new letter.
  • WebSocket (Phone Call): You dial a number. The server answers. Instead of hanging up, you both keep the phone line active. You can speak, the server can speak back immediately, and neither of you has to re-dial the phone. You only hang up (Close Connection) when the conversation is finished.
1. HTTP Handshake Upgrade
Upgrade request GET /ws
2. Connection Upgraded
TCP socket established
3. Full-Duplex Flow
Both sides stream data anytime

Core Specifications

  • Why it exists: It was created to enable persistent, bidirectional, full-duplex TCP communication channels between client browsers and backend services.
  • What problem it solves: It removes the network latency, high CPU overhead, and wasted bandwidth of short/long HTTP polling cycles, allowing servers to push updates to clients instantly.
  • How it works internally: It initiates as a standard HTTP GET request containing upgrade request headers (Upgrade: websocket). If the server supports the protocol, it returns an HTTP 101 Switching Protocols response, upgrading the TCP connection to a frame-based protocol tunnel that remains open until explicitly closed.
  • When to use it: Use it for live streaming feeds, chat rooms, multiplayer collaborative canvases, co-presence tracking, and real-time dashboard notifications.
  • How it is used in real projects: Collaborative whiteboard apps bind cursor mouse movements to client socket send handlers, broadcasting drawing and position payloads to other connected clients in real-time.

Codebase Integration

Here is how to set up a real-time WebSocket server and connect a client in Node.js and TypeScript.

Step 1: Install Dependencies

Install the standard ws socket library and its dev dependency typings.

# In your server root directory
npm install ws
npm install @types/ws --save-dev

Step 2: Create the WebSocket Server Instance

Initialize the server class. Put this inside src/lib/socket.ts to coordinate connection setups.

// filepath: src/lib/socket.ts
// Purpose: Initialize the WebSocket Server and export connection attachment functions.

import { WebSocketServer, WebSocket } from 'ws';
import { Server } from 'http';

// Store all active, connected clients
export const clients = new Set<WebSocket>();

export function initWebSocketServer(server: Server) {
  const wss = new WebSocketServer({ noServer: true });

  wss.on('connection', (socket: WebSocket) => {
    clients.add(socket);
    console.log(`⚡ Client joined! Active connections: ${clients.size}`);

    // Listen for client messages
    socket.on('message', (message: string) => {
      console.log(`Received: ${message}`);
      
      // Broadcast the message to all connected clients
      for (const client of clients) {
        if (client.readyState === WebSocket.OPEN) {
          client.send(message.toString());
        }
      }
    });

    // Handle client disconnection
    socket.on('close', () => {
      clients.delete(socket);
      console.log(`❌ Client disconnected. Active connections: ${clients.size}`);
    });
  });

  return wss;
}

Step 3: Wire the socket listener to your Server entry point

Attach the WebSocket initialization logic to your standard HTTP Server startup file, e.g. src/server.ts.

// filepath: src/server.ts
// Purpose: Attach WebSocket protocol switching hooks to the express HTTP server.

import express from 'express';
import { createServer } from 'http';
import { initWebSocketServer } from './lib/socket';

const app = express();
const server = createServer(app);

const wss = initWebSocketServer(server);

// Intercept standard HTTP upgrades and hand them over to our socket handler
server.on('upgrade', (request, socket, head) => {
  wss.handleUpgrade(request, socket, head, (ws) => {
    wss.emit('connection', ws, request);
  });
});

server.listen(4000, () => {
  console.log('HTTP and WebSocket server running on http://localhost:4000');
});

Step 4: Write client-side browser listener logic

Write client connections inside your frontend codebase, e.g. src/components/ChatRoom.tsx.

// filepath: src/components/ChatRoom.tsx
// Purpose: Establish browser socket connection and listen for chat broadcasts.

import React, { useEffect, useState } from 'react';

export default function ChatRoom() {
  const [socket, setSocket] = useState<WebSocket | null>(null);
  const [messages, setMessages] = useState<string[]>([]);
  const [input, setInput] = useState('');

  useEffect(() => {
    // Open connection to WebSocket Server
    const ws = new WebSocket('ws://localhost:4000');
    setSocket(ws);

    // Listen for incoming messages
    ws.onmessage = (event) => {
      setMessages((prev) => [...prev, event.data]);
    };

    return () => {
      ws.close();
    };
  }, []);

  const sendMessage = () => {
    if (socket && input.trim()) {
      socket.send(input);
      setInput('');
    }
  };

  return (
    <div className="p-4 border rounded-lg max-w-md bg-card-base border-card-border">
      <div className="h-60 overflow-y-auto mb-4 p-2 bg-muted-bg rounded border">
        {messages.map((msg, i) => (
          <p key={i} className="text-sm border-b py-1">{msg}</p>
        ))}
      </div>
      <div className="flex gap-2">
        <input 
          className="border rounded p-2 flex-1 text-sm bg-card-base"
          value={input}
          onChange={(e) => setInput(e.target.value)}
        />
        <button className="bg-primary text-white rounded px-4 py-2 text-sm" onClick={sendMessage}>
          Send
        </button>
      </div>
    </div>
  );
}

Practical Project Use Cases

1. Multiplayer Collaborative Document Cursor Tracking

In remote workspace applications (like Figma or Notion), seeing where other team members' cursor pointers are located in real-time is crucial for interactive co-presence.

  • Real-World Example: We track user cursor motions on a canvas or canvas editor pane, scale the client-side pixel boundaries, and broadcast the coordinate data via WebSocket connections to all other active editors.
// filepath: src/components/MultiplayerCanvas.tsx
// Purpose: Track client mouse events and broadcast coordinate changes over the socket channel.

import React, { useEffect, useState, useRef } from 'react';

interface Cursor {
  userId: string;
  x: number;
  y: number;
}

export default function MultiplayerCanvas() {
  const [socket, setSocket] = useState<WebSocket | null>(null);
  const [cursors, setCursors] = useState<Record<string, Cursor>>({});
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    // Connect to WebSocket Server
    const ws = new WebSocket('ws://localhost:4000');
    setSocket(ws);

    // Listen for incoming cursor updates
    ws.onmessage = (event) => {
      try {
        const payload = JSON.parse(event.data);
        if (payload.type === 'CURSOR_MOVE') {
          setCursors((prev) => ({
            ...prev,
            [payload.userId]: { userId: payload.userId, x: payload.x, y: payload.y }
          }));
        }
      } catch (err) {
        console.error('Error parsing canvas payload', err);
      }
    };

    return () => {
      ws.close();
    };
  }, []);

  const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
    if (!socket || socket.readyState !== WebSocket.OPEN || !containerRef.current) return;

    const rect = containerRef.current.getBoundingClientRect();
    const x = event.clientX - rect.left;
    const y = event.clientY - rect.top;

    socket.send(
      JSON.stringify({
        type: 'CURSOR_MOVE',
        userId: 'user_anonymous', // In practice, dynamic from auth context
        x,
        y,
      })
    );
  };

  return (
    <div 
      ref={containerRef}
      onMouseMove={handleMouseMove}
      className="relative w-full h-80 border rounded-lg bg-card-base border-card-border overflow-hidden cursor-crosshair"
    >
      <div className="absolute top-2 left-2 text-xs text-muted-fg pointer-events-none">
        Hover here to broadcast cursor position to other connected socket screens
      </div>
      {Object.values(cursors).map((cursor) => (
        <div
          key={cursor.userId}
          className="absolute w-4 h-4 text-primary transition-all duration-75 pointer-events-none"
          style={{ left: cursor.x, top: cursor.y }}
        >
          <svg className="w-4 h-4 fill-current text-primary" viewBox="0 0 24 24">
            <path d="M4 4l16 16-6.5-1.5L10 20z" />
          </svg>
          <span className="absolute left-4 -top-2 bg-primary text-white text-[10px] px-1 rounded whitespace-nowrap">
            {cursor.userId}
          </span>
        </div>
      ))}
    </div>
  );
}

2. Financial Stock Ticker and Live Order-Book Syncing

For high-frequency trading terminals or crypto dash screens, fetching prices every second via REST polling overwhelms servers and introduces latency that could cost users money.

  • Real-World Example: A backend microservice subscribes to a Kafka/Redis queue, forwards trade executions, and streams ticks continuously over dedicated socket connection tunnels directly onto browser stock grids.

Common Pitfalls

1. File Descriptor Leak on Scaling

Every WebSocket connection must stay open. In standard Unix systems, every active connection is a File Descriptor. If you have 50,000 users, your operating system will run out of file descriptors and block new connections.

  • Fix: Edit target system configurations to raise file limits (ulimit -n 65535), and distribute active connections across load-balanced instances using Redis Pub/Sub to sync events.

2. Assuming Sockets automatically reconnect

If a user goes through a tunnel or Wi-Fi drops, the socket connection will die. The browser will not reconnect automatically.

  • Fix: Write browser reconnection wrappers with exponential backoff.

Interview Questions

How does the HTTP protocol switch to the WebSocket protocol?

The connection starts as a standard HTTP GET request with specific upgrade headers, including Upgrade: websocket and Sec-WebSocket-Key. If the server supports the protocol, it accepts the switch by responding with HTTP status code 101 Switching Protocols. At this point, the underlying TCP socket remains open and switches to streaming frames bidirectional.


Summary

  • Bidirectional: Allows server pushes without polling delays.
  • Format: Upgrades from HTTP to permanent TCP protocol tunnel.
  • Scaling: Requires raising system OS connection limits and synchronizing multiple servers.

Keep Learning

system-design

Redis In-Memory Caching

Why Redis is so fast, how in-memory caching works, and a step-by-step implementation guide.

7 min readRead
database

PostgreSQL Relationships

Master One-to-One, One-to-Many, and Many-to-Many relationships in PostgreSQL with step-by-step migration scripts.

8 min readRead