WebSockets
One connection both sides can write to, opened by upgrading an HTTP request, with its own handlers per client.
wss.on('connection', (ws, req) => { ws.on('message', (data) => ws.send(data.toString())); });A WebSocket is a single connection that both sides can write to, for as long as it stays open. Server-Sent Events only go one way. A WebSocket goes both ways, which is what a chat box, a game, or a collaborative editor needs.
The upgrade
It starts as an HTTP GET carrying Upgrade: websocket. The server answers 101 Switching Protocols and that socket stops speaking HTTP. Node's HTTP server emits 'upgrade' instead of 'request' for those, with three arguments: the request, the raw socket, and head, the first bytes that arrived after the headers. In Node.js 24.9.0 a shouldUpgradeCallback option on createServer was added to decide which upgrades reach the event.
Node ships a WebSocket client: the global WebSocket, added in v21.0.0 and no longer experimental since v22.4.0. It ships no server. For the server side the ws package is the standard choice, because it handles the handshake, the message framing and the closing handshake.
One handler pair per client
The 'connection' callback runs once per client. Inside it you attach that client's own 'message' and 'close' handlers, and anything you declare there is per-connection state. Three constructor forms describe the same server:
new WebSocketServer({ port: 8080 }); // shorthand: makes its own HTTP server
new WebSocketServer({ server }); // shares an HTTP server you created
new WebSocketServer({ noServer: true }); // you call wss.handleUpgrade yourself
What trips people up
After the 101 there are no status codes left, so reject a bad token in the upgrade handler, before the switch. And ws hands message data over as a Buffer, so comparing it to a string fails until you call data.toString().
Example
When to use it
- Deliver chat messages in both directions over one connection.
- Send cursor and edit positions between people in a shared document.
- Accept a live feed from a provider that only publishes over WebSocket.
- Push changes to a page that also needs to send input back quickly.
More examples
A broadcast server with the ws package
The standard chat-room shape: wss.clients is the set of live sockets, and every message is relayed to the others. The readyState check is what keeps a closing socket from throwing mid-broadcast.
// npm install ws
const http = require('node:http');
const { WebSocketServer, WebSocket } = require('ws');
const server = http.createServer(); // your normal HTTP server
const wss = new WebSocketServer({ server }); // shares its port
wss.on('connection', (ws, req) => {
// Per-connection state: one of these objects exists per client.
const client = { ip: req.socket.remoteAddress };
ws.on('message', (data, isBinary) => {
const text = data.toString(); // data arrives as a Buffer
for (const peer of wss.clients) {
// A socket that is closing is still in the set; sending to it throws.
if (peer !== ws && peer.readyState === WebSocket.OPEN) {
peer.send(text, { binary: isBinary });
}
}
});
ws.on('close', (code, reason) => {
console.log('left', client.ip, code, reason.toString());
});
});
server.listen(8080);Check the token before upgrading
Reach for noServer whenever a connection needs authorising, routing by path, or rate limiting. It is the only place a rejection can still be an HTTP response the browser can read.
const { WebSocketServer } = require('ws');
// noServer means ws does the framing but you run the handshake yourself.
const wss = new WebSocketServer({ noServer: true });
server.on('upgrade', (req, socket, head) => {
const url = new URL(req.url, 'http://localhost'); // req.url is path + query
const token = url.searchParams.get('token');
if (!verifyToken(token)) {
// After 101 there is no status code to send, so refuse while still HTTP.
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
return;
}
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req); // hands it to the normal connection flow
});
});Node's built-in client
Useful for a service that consumes someone else's WebSocket feed, and for integration tests of your own server. Note the two different event styles: browser-shaped on the built-in client, EventEmitter-shaped on ws sockets.
// The global WebSocket client: added in Node.js v21.0.0, no longer
// experimental since v22.4.0. No package needed.
const ws = new WebSocket('ws://127.0.0.1:8080');
// This client follows the browser API, so it uses addEventListener,
// not the .on() form the ws package gives its sockets.
ws.addEventListener('open', () => {
ws.send(JSON.stringify({ type: 'hello' }));
});
ws.addEventListener('message', (event) => {
console.log('from server:', event.data); // a string for a text message
});
ws.addEventListener('close', (event) => {
console.log('closed', event.code, event.reason);
});
Discussion