Skip to main content
Back to blog

Understanding WebSockets for real-time features

·4 min readWeb Dev

HTTP is request-response. The client asks, the server answers, the connection closes. This works for most of the web, but it falls apart when you need real-time updates: chat messages, live notifications, collaborative editing, stock tickers. Polling the server every second is wasteful. WebSockets solve this.

How WebSockets work

A WebSocket connection starts as an HTTP request (the "upgrade" handshake) and then switches to a persistent, bidirectional connection. Both the client and server can send messages at any time without the overhead of establishing a new connection each time.

HTTP:       Client -> Request -> Server -> Response -> Connection closed
WebSocket:  Client <-> Server (persistent, bidirectional)
sequenceDiagram
    participant C as Client
    participant S as Server

    rect rgb(50, 50, 70)
    note over C,S: HTTP (request-response)
    C->>S: Request
    S-->>C: Response
    note over C,S: Connection closed
    end

    rect rgb(40, 70, 50)
    note over C,S: WebSocket (persistent)
    C->>S: Upgrade handshake
    S-->>C: 101 Switching Protocols
    C->>S: Message
    S->>C: Message
    S->>C: Message
    C->>S: Message
    note over C,S: Connection stays open
    end

The connection stays open until either side closes it. Messages flow in both directions with minimal overhead (just a few bytes of framing per message, compared to hundreds of bytes of HTTP headers per request).

A practical example

Here is a minimal WebSocket server with Node.js using the ws library:

import { WebSocketServer } from "ws";
 
const wss = new WebSocketServer({ port: 8080 });
 
wss.on("connection", (ws) => {
  console.log("Client connected");
 
  ws.on("message", (data) => {
    const message = data.toString();
    console.log("Received:", message);
 
    // Broadcast to all connected clients
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(message);
      }
    });
  });
 
  ws.on("close", () => {
    console.log("Client disconnected");
  });
});

And the client:

const ws = new WebSocket("ws://localhost:8080");
 
ws.onopen = () => {
  ws.send("Hello from client");
};
 
ws.onmessage = (event) => {
  console.log("Received:", event.data);
};
 
ws.onclose = () => {
  console.log("Disconnected");
};

That is a working chat server in about 20 lines per side.

When to use WebSockets

Chat and messaging. The classic use case. Messages need to arrive instantly without the client polling.

Live notifications. When something happens on the server (new order, deployment finished, build failed), push it to the client immediately.

Collaborative editing. Multiple users editing the same document need to see each other's changes in real time.

Live dashboards. Monitoring tools, analytics dashboards, and status pages that update without refreshing.

When NOT to use WebSockets

CRUD operations. Creating, reading, updating, and deleting data is perfectly served by REST. Adding WebSockets for a contact form is overengineering.

Infrequent updates. If data changes every few minutes, simple polling or Server-Sent Events (SSE) is simpler. WebSockets shine when updates are frequent and unpredictable.

Public APIs. WebSockets are harder to cache, proxy, and load balance than HTTP. For public-facing APIs, REST or GraphQL is more practical.

Server-Sent Events: the simpler alternative

If you only need server-to-client updates (no bidirectional communication), Server-Sent Events are simpler:

// Server (Express)
app.get("/events", (req, res) => {
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");
 
  const interval = setInterval(() => {
    res.write(`data: ${JSON.stringify({ time: new Date() })}\n\n`);
  }, 1000);
 
  req.on("close", () => clearInterval(interval));
});
 
// Client
const source = new EventSource("/events");
source.onmessage = (event) => {
  console.log(JSON.parse(event.data));
};

SSE works over regular HTTP, reconnects automatically, and is simpler to set up. Use it when the server needs to push updates but the client does not need to send messages back through the same channel.

Connection management

WebSocket connections need care:

Reconnection. Connections drop. Networks switch. Implement automatic reconnection with exponential backoff.

Heartbeats. Send periodic ping/pong messages to detect dead connections. Without this, you might not notice a client disconnected until you try to send it a message.

Authentication. Pass a token during the initial HTTP upgrade handshake. Do not rely on cookies alone for WebSocket authentication.

Sources

Enjoying the blog? Subscribe via RSS to get new posts in your reader.

Subscribe via RSS