NodeJSWebSockets (ws)

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

Text
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 begins as HTTP, then upgrades (101 Switching Protocols) to a persistent two-way channel
The connection starts as a normal HTTP `GET` carrying `Upgrade: websocket` headers. If the server agrees, it responds `101 Switching Protocols` and from that point the same TCP connection is "upgraded" — it stops speaking HTTP and becomes a full-duplex WebSocket over which both sides exchange lightweight **frames**. Because the handshake *is* HTTP, WebSockets share ports 80/443 with your web traffic and pass through most HTTP infrastructure, and `wss://` (WebSocket over TLS) gives you the same encryption as `https://`. After the upgrade there's no per-message HTTP overhead — just small framed payloads — which is why WebSockets are far more efficient than polling for high-frequency, two-way communication.
A WebSocket server with ws

Bash
npm install ws

JS
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
})
Each `connection` gives you a `ws` socket — `.send()` to push, `.on('message')` to receive, both any time
The `WebSocketServer` emits a `connection` event for each client, handing you a `ws` socket object representing that one connection. On it, `ws.send(data)` pushes a message to that client and `ws.on('message', ...)` receives messages from it — and crucially, *both can happen at any time*, in any order, for the life of the connection. Incoming data arrives as a `Buffer` (call `.toString()` for text, or `JSON.parse(data)` for JSON protocols). Always handle `close` (to clean up per-connection state) and `error` (so a single bad socket doesn't crash the process).
Broadcasting to all clients

JS
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
})
`wss.clients` is the set of connections — always check `readyState === OPEN` before sending
To build chat-like features you broadcast by iterating `wss.clients` (the `Set` of all current connections) and calling `send` on each. The essential guard is checking `client.readyState === WebSocket.OPEN` first: a connection may be mid-close or already closed, and sending to it throws or silently fails. For real apps you'll typically track richer structure than the flat client set — mapping sockets to user IDs and "rooms"/channels so you can target subsets (everyone in room X, this specific user). [Socket.IO](/nodejs/socket-io) provides rooms and broadcasting helpers out of the box; with raw `ws` you maintain that bookkeeping yourself.
Heartbeats — detecting dead connections

JS
// 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))
Dead connections don't always fire `close` — use ping/pong heartbeats or you'll leak zombie sockets
A WebSocket can die *silently*: a client's laptop sleeps, Wi-Fi drops, or a network device kills the connection without a clean TCP close — so the server never receives a `close` event and keeps the socket (and its associated state and memory) forever. These **zombie connections** accumulate and leak resources. The standard defense is a **ping/pong heartbeat**: the server periodically `ping`s each client (which auto-replies with `pong`), and any socket that misses a round gets `terminate()`d. This actively detects and reaps dead connections instead of waiting for a close event that may never come. It also keeps connections alive through proxies that close idle ones.
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 Origin header — 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.

WebSockets bypass CORS and per-request auth — authenticate the handshake and check `Origin` yourself
WebSocket connections do **not** follow the same security model as fetch/XHR: the browser's CORS rules don't apply, and there's no per-request auth middleware after the upgrade. This means two things you must handle explicitly. First, **authenticate at the handshake** — verify the user's [JWT](/nodejs/jwt) or session in the upgrade request and reject unauthenticated connections immediately, rather than accepting them and hoping. Second, **validate the `Origin` header** yourself, because without it any website the user visits can open a WebSocket to your server using the user's cookies (cross-site WebSocket hijacking, the WebSocket cousin of [CSRF](/nodejs/csrf-protection)). After connecting, keep authorizing: every incoming message is untrusted input.
Scaling WebSockets
Across multiple instances, use a [Redis](/nodejs/redis) pub/sub backplane so a message reaches clients on every instance
Like all [persistent connections](/nodejs/realtime-intro), WebSockets are stateful and bound to the instance that holds them, so scaling [horizontally](/nodejs/scaling-strategies) requires a **backplane**: when instance A needs to broadcast, it can only reach *its own* connected sockets directly — clients on instances B and C are invisible. Publishing events through [Redis](/nodejs/redis) pub/sub (each instance subscribes and relays to its local sockets) fans messages out across all instances. You also need a load balancer configured for WebSocket upgrades, and either sticky routing or a stateless design. Plan for memory-per-connection limits, reconnection on the client, and recovery of messages missed during a disconnect.
Next
A batteries-included library on top of WebSockets: [Socket.IO](/nodejs/socket-io).