WebSockets (ws)
A WebSocket is a persistent, full-duplex connection between client and server over a single TCP socket — once established, either side can send messages at any time with minimal overhead. It starts life as an HTTP request that gets upgraded to the WebSocket protocol, then drops the request/response model entirely. The ws library is the de-facto WebSocket implementation for Node: small, fast, and standards-compliant. This page covers the handshake, building a server and client with ws, broadcasting, heartbeats to detect dead connections, authentication, and the production concerns of scaling.
The upgrade handshake
Client → Server (an ordinary HTTP request asking to upgrade):
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Server → Client (agrees, switching protocols):
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
→ After 101, it's no longer HTTP. The TCP connection stays open as a
bidirectional WebSocket; either side sends "frames" whenever it wants.A WebSocket server with ws
npm install ws
import { WebSocketServer } from 'ws'
const wss = new WebSocketServer({ port: 8080 })
wss.on('connection', (ws, req) => {
console.log('client connected from', req.socket.remoteAddress)
ws.on('message', (data) => {
const msg = data.toString() // data is a Buffer
console.log('received:', msg)
ws.send(`echo: ${msg}`) // send back to THIS client
})
ws.on('close', () => console.log('client disconnected'))
ws.on('error', (err) => console.error('socket error:', err))
ws.send('welcome!') // server can push immediately
})Broadcasting to all clients
import { WebSocketServer, WebSocket } from 'ws'
const wss = new WebSocketServer({ port: 8080 })
function broadcast(message, except) {
for (const client of wss.clients) {
// Only send to sockets that are actually open (skip closing/closed):
if (client !== except && client.readyState === WebSocket.OPEN) {
client.send(message)
}
}
}
wss.on('connection', (ws) => {
ws.on('message', (data) => broadcast(data.toString(), ws)) // relay to everyone else
})Heartbeats — detecting dead connections
// TCP can drop silently (laptop sleeps, network dies) without a 'close' event.
// Ping every client periodically; terminate ones that don't pong back.
function heartbeat() { this.isAlive = true }
wss.on('connection', (ws) => {
ws.isAlive = true
ws.on('pong', heartbeat) // client auto-replies to ping with pong
})
const interval = setInterval(() => {
for (const ws of wss.clients) {
if (ws.isAlive === false) return ws.terminate() // missed last pong → dead
ws.isAlive = false
ws.ping() // expect a pong before next tick
}
}, 30000)
wss.on('close', () => clearInterval(interval))Authentication and security
Authenticate during the handshake — validate a JWT or session cookie in the upgrade request before accepting the connection; don't wait until after.
Always use
wss://(TLS) in production —ws://is plaintext and trivially intercepted.Check the
Originheader — reject upgrades from origins you don't expect to prevent cross-site WebSocket hijacking.Validate and authorize every message — a client can send anything once connected; never trust message contents or assume the user may perform the action.
Rate-limit messages per connection — a malicious client can flood you; cap message rate and payload size.
Set a max payload size (
maxPayload) so a huge frame can't exhaust memory.