Web Developmentintermediate8 min read

WebSocket Protocol Guide

Understand how WebSockets work, when to use them over HTTP, and how to implement real-time bidirectional communication in your applications.

What WebSockets Solve

HTTP is request-response: the client asks, the server answers, and the connection closes. For real-time features β€” chat, live dashboards, collaborative editing, multiplayer games β€” polling HTTP is wasteful. Each poll opens a new TCP connection, sends headers, waits, and closes. At high frequency this burns CPU and bandwidth on both sides.

WebSocket upgrades an existing HTTP connection into a persistent, full-duplex channel. Once open, either side can send a frame at any time without the overhead of a new handshake. A single TCP connection replaces hundreds of polling requests.

The Handshake

WebSocket starts as HTTP. The client sends an Upgrade request; if the server agrees, it responds with 101 Switching Protocols and both sides flip into WebSocket framing mode.

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

After the server replies with 101 Switching Protocols, HTTP is gone. The connection is now a raw WebSocket stream and either side can send frames at any time.

Implementing a WebSocket Server

Node.js ws is the minimal, dependency-light choice. For production use with rooms, namespaces, and automatic reconnection, Socket.IO wraps ws with a higher-level API.

import { WebSocketServer } from "ws";

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

wss.on("connection", (socket) => {
  socket.on("message", (data) => {
    for (const client of wss.clients) {
      if (client.readyState === WebSocket.OPEN) {
        client.send(data.toString());
      }
    }
  });
});

Browser API

The browser WebSocket constructor connects synchronously but the handshake is async. Attach event handlers before the connection opens or you may miss early messages.

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

ws.addEventListener("open", () => ws.send(JSON.stringify({ type: "join" })));
ws.addEventListener("message", (event) => {
  const msg = JSON.parse(event.data);
  console.log("received:", msg);
});
ws.addEventListener("close", (event) => {
  console.log("closed:", event.code, event.reason);
});

When Not to Use WebSockets

WebSockets are not always the right tool:

  • Simple notifications to the client β€” use Server-Sent Events (SSE) instead. SSE is unidirectional, works over plain HTTP/2, and reconnects automatically.
  • Infrequent updates β€” polling every 30 seconds is simpler to deploy and debug than a persistent socket.
  • Multiple server instances β€” WebSockets require sticky sessions or a pub/sub broker (Redis, NATS) to work across instances. Plan for this before scaling horizontally.

Use WebSockets when you need low-latency bidirectional messaging at high frequency and the infrastructure complexity is justified.

Production Considerations

Running WebSockets at scale introduces operational concerns that HTTP endpoints don't have:

  • Connection limits β€” each open socket holds a file descriptor. Tune ulimit -n and net.core.somaxconn for high-concurrency servers.
  • Heartbeats β€” proxies (nginx, AWS ALB) time out idle connections. Send a ping frame every 25–30 seconds to keep them alive.
  • Graceful shutdown β€” send a 1001 Going Away close frame and drain open sockets before restarting the server process.
  • TLS β€” always use wss:// in production. Plain ws:// traffic is trivially intercepted.

Related Tutorials