A WebSocket is a single connection between a browser and a server that stays open, so either side can send a message the moment it has something to say. It is how chat apps, live scores and multiplayer games stay up to date without the page asking the server for news every few seconds.
Why asking again and again falls short
Ordinary HTTP works one way round: the client asks, the server answers, and the exchange is over. The server can't speak first. If your page shows a live feed (new messages, new orders, new tuna arriving at the harbour), it has to find out about changes somehow.
The simplest fix is polling: ask on a timer.
setInterval(async () => {
const res = await fetch("/api/tuna/latest");
const tuna = await res.json();
render(tuna);
}, 3000);It works, but it has two costs. Most of those requests come back with nothing new, and every one of them carries full HTTP headers, cookies and, without a reused connection, a fresh handshake. And an update that lands just after a poll waits until the next one, so 'live' really means 'up to three seconds late'. Shorten the interval to cut the delay and you multiply the wasted requests.
Long polling improves on this: the server holds each request open until it has something to send, then the client immediately asks again. It cuts the delay, but it is still one request per update, and the server only ever replies to a question. WebSockets remove the question altogether.
How a WebSocket connection opens
A WebSocket starts life as an ordinary HTTP request, which is why it gets through the same ports and proxies as the rest of the web. The client asks the server to switch protocols:
GET /tuna HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13If the server supports WebSockets on that path, it agrees with a 101 Switching Protocols response:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=The Sec-WebSocket-Accept value is worked out from the client's key, which proves the server understood the upgrade request rather than being a cache replaying an old answer. After the 101, nobody speaks HTTP on that connection again. The same TCP connection stays open and now carries WebSocket messages, in both directions, until one side closes it.
Here is the whole change, from polling to a server that pushes:
Polling, then a WebSocket that pushes updates
Step 1 of 8: With polling, the app sends a fresh HTTP request to ask for news.
The address uses its own schemes: ws:// for a plain connection and wss:// for one encrypted with TLS, the WebSocket equivalent of https://. Use wss:// in production.
Messages, frames and keeping the line alive
Once the connection is open, each side sends messages, either text (usually JSON) or binary data. On the wire a message travels as one or more frames, each with a header of just a few bytes. Compare that with an HTTP request, which repeats its method, path, headers and cookies every time, and you can see why a busy real-time app saves so much traffic.
There are also small control frames that you rarely handle yourself:
- Ping and pong frames check the other side is still there. Servers send pings, and browsers answer with pongs automatically.
- A close frame ends the connection cleanly, with a code such as
1000for a normal close and an optional reason.
A WebSocket gives you a pipe, not a format. Nothing in the protocol says what a message means, so most apps agree a small envelope, such as { "type": "tuna", "count": 3 }, and switch on the type field.
A worked example: a live tuna tracker
In the browser, the built-in WebSocket class does everything. You open the connection, then react to events:
const socket = new WebSocket("wss://example.com/tuna");
socket.addEventListener("open", () => {
socket.send(JSON.stringify({
type: "watch",
harbour: "north",
}));
});
socket.addEventListener("message", (event) => {
const update = JSON.parse(event.data);
if (update.type === "tuna") {
console.log("New tuna:", update.count);
}
});
socket.addEventListener("close", (event) => {
console.log("Closed with code", event.code);
});Notice that send is only called once the open event has fired. Calling it while the socket is still connecting throws an error.
On the server, Node.js developers often use the ws package. This server greets each new client, then pushes every new arrival to everyone connected:
import { WebSocketServer, WebSocket } from "ws";
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (socket) => {
socket.send(JSON.stringify({ type: "hello" }));
});
export function announceTuna(count) {
const message = JSON.stringify({ type: "tuna", count });
for (const client of wss.clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
}
}Whatever calls announceTuna, a database change or a message from another service, every open tab hears about it straight away. That loop over every connected client is called a broadcast, and most chat rooms and live dashboards are built on it.
When to use WebSockets, and when not to
WebSockets suit anything where both sides need to talk often and with little delay:
- chat and messaging;
- live feeds, scores and dashboards;
- multiplayer games, where every player's moves go to everyone else;
- collaborative editors, where several people type into one document.
They are not the answer to every 'live' feature. Each open WebSocket is a long-lived connection the server has to keep in memory, so they cost more to run than stateless HTTP requests.
| Need | Reach for |
|---|---|
| Updates both ways, often | WebSockets |
| Server-to-client updates only | Server-Sent Events |
| Changes every few minutes | Polling |
Server-Sent Events (the browser's EventSource) stream updates from the server over a plain HTTP response. If the client never needs to send anything back on the same channel, as with a news ticker or a progress bar, they are simpler and reconnect on their own. And if the data changes rarely, a poll every minute is cheap and easy to reason about.
Common mistakes
- No reconnection: connections drop when a phone changes network or the server redeploys. The browser's
WebSocketdoesn't reconnect by itself, so listen forcloseand reconnect with a growing delay, then fetch anything missed while you were away. - Skipping the Origin check: WebSockets aren't covered by the same-origin rules that protect
fetch, so CORS doesn't help you here. The browser sends cookies with the upgrade request, so a malicious site could open a socket to your server as your logged-in user. Check theOriginheader during the handshake and reject sites you don't recognise. - Forgetting authentication: the browser's
WebSocketcan't set custom headers such asAuthorization. Rely on a cookie checked at the handshake, or send a token in the first message and refuse everything until it checks out. - Silent dead connections: proxies and load balancers close connections that sit idle. Have the server send pings, or swap a small heartbeat message, every so often to keep them open and to spot ones that have died.
- Forgetting about scale: a client is connected to one server. With several servers behind a load balancer, a broadcast on one server only reaches its own clients, so the servers need a shared channel, such as a publish/subscribe service, to pass messages to each other. The load balancer must support the upgrade, too.
Key takeaways
- A WebSocket is one long-lived connection that both client and server can send on at any time.
- It opens as an HTTP request with an
Upgradeheader, and the server answers101 Switching Protocols. - Messages travel in small frames, so frequent updates cost far less than repeated HTTP requests.
- Use it for chat, live feeds, games and collaboration; use Server-Sent Events for one-way updates and polling for rare ones.
- Plan for reconnection, check the
Originheader, authenticate the connection and usewss://.