What are WebSockets?
A WebSocket is a persistent, full-duplex connection between a client and a server. After an initial HTTP handshake, both sides can send messages at any time over the same TCP connection, with very little per-message overhead. The server can push data the moment something changes instead of waiting for the client to ask.
That makes WebSockets the right tool for chat, presence, live dashboards, collaborative editing, multiplayer games and anything else where updates must arrive immediately. For simple one-way updates, Server-Sent Events are often a simpler choice.
The upgrade handshake
A WebSocket connection begins life as an HTTP request.
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
If the server accepts, it replies:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
After 101 Switching Protocols, the connection is no longer HTTP. It becomes a WebSocket, and data flows in frames. Because it starts as HTTP, it works over the same ports (80 and 443), through most proxies and firewalls, and upgrades to TLS with wss://.
Frames
WebSocket messages are split into frames with a small header. There are text frames, binary frames and control frames for ping, pong and close. The framing is minimal, which is why WebSocket messages are far cheaper than repeated HTTP requests.
The protocol also defines close codes, such as 1000 for a normal closure and 1001 for going away, which help both sides understand why a connection ended.
The browser API
The client API is small and event-driven.
// client.js
const socket = new WebSocket("wss://api.example.com/chat");
socket.addEventListener("open", () => {
socket.send(JSON.stringify({ type: "join", room: "general" }));
});
socket.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
render(message);
});
socket.addEventListener("close", (event) => {
console.log("closed", event.code, event.reason);
});
socket.addEventListener("error", () => {
console.error("connection error");
});
You send with socket.send() and receive message events. The readyState property tells you whether the socket is connecting, open, closing or closed.
A Node.js server
The ws library is the standard way to run a WebSocket server in Node.
// server.js
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (socket, request) => {
socket.on("message", (data, isBinary) => {
for (const client of wss.clients) {
if (client !== socket && client.readyState === client.OPEN) {
client.send(data, { binary: isBinary });
}
}
});
socket.on("close", () => console.log("client left"));
});
The server tracks every connection and can send to one client, a subset (a room) or all of them. Socket.IO is a higher-level alternative that adds rooms, acknowledgements, automatic reconnection and fallbacks, at the cost of a custom protocol and client.
Rooms and presence
Real applications rarely broadcast to everyone. You track which connection belongs to which user or room.
// rooms.js
const rooms = new Map();
function join(socket, room) {
if (!rooms.has(room)) rooms.set(room, new Set());
rooms.get(room).add(socket);
}
function broadcast(room, payload) {
for (const socket of rooms.get(room) ?? []) {
if (socket.readyState === socket.OPEN) socket.send(payload);
}
}
Rooms, presence (who is online) and typing indicators are all built from this pattern plus heartbeats to detect disconnects.
Heartbeats and reconnection
A connection can die without either side noticing, especially on mobile networks or behind proxies. Two things keep it healthy:
- Heartbeats. Send a ping every 30 seconds and terminate the connection if no pong arrives. Many proxies also close idle connections, so traffic keeps them open.
- Reconnection. The client should reconnect with exponential backoff and re-establish its subscriptions. This is where a library like Socket.IO saves effort.
// heartbeat.js
const interval = setInterval(() => {
for (const socket of wss.clients) {
if (socket.isAlive === false) return socket.terminate();
socket.isAlive = false;
socket.ping();
}
}, 30_000);
wss.on("close", () => clearInterval(interval));
Scaling
A WebSocket connection lives on exactly one server instance, so broadcasting across instances needs a shared channel.
- Pub/sub: publish messages to Redis, NATS or another broker, and have every instance deliver them to its local clients.
- Sticky sessions: if your infrastructure requires them, keep a client on the same instance for the life of the connection.
- State in a shared store: keep presence and room membership in Redis so any instance can answer.
- Backpressure and limits: cap connections per instance and monitor memory, because each connection holds a socket and some state.
Security
- Always use
wss://in production so traffic is encrypted. - Authenticate during the handshake and reject unauthorised connections.
- Validate the
Originheader to prevent cross-site WebSocket hijacking. - Authorise every message; never trust that a connected client is allowed to do something.
- Limit message size and rate to prevent abuse.
- Set timeouts so abandoned connections do not leak.
Best practices
- Use WebSockets for bidirectional, low-latency updates and SSE for one-way push.
- Heartbeat and reconnect; never assume a connection is alive.
- Authenticate during the handshake and authorise every message.
- Keep per-connection state small and rooms in a shared store.
- Use pub/sub to broadcast across instances.
- Set message size and rate limits.
- Terminate idle connections and monitor connection counts.
Common mistakes
- Using WebSockets for simple request/response where HTTP would do.
- Forgetting heartbeats and leaking dead connections.
- Broadcasting to every client when only a room should receive.
- Storing authoritative state only in memory and losing it on restart.
- Skipping origin checks and message authorisation.
- Assuming the load balancer will preserve the connection without configuration.
Where to go next
WebSockets are the persistent-connection layer on top of TCP. Ground them in the HTTP guide and TCP/IP & Sockets, run them on Node.js, and compare the push model with GraphQL subscriptions. Then build a small chat room with rooms and heartbeats to see the patterns in practice.