mrkeyoor.com_
Wed 05 Aug 05:02 UTC
npmWeb Backendupdated 05 Aug 2026

ws

ws is the standard WebSocket client and server implementation for Node.js. It gives you a spec-compliant WebSocketServer plus a client class for talking to other backends, passes the extensive Autobahn protocol test suite, and has zero required dependencies. Much of Node realtime tooling, including Socket.IO's engine, is built on top of it. One thing it is not: a browser library. Browsers use the native WebSocket object; ws only runs in Node.

Verdict

The default choice for WebSockets in Node and it has earned it: tiny, fast, protocol-correct, and actively maintained. Just know it is a protocol library, not a realtime framework; you will write your own reconnect and heartbeat logic.

API stability5/5v8 has been the current major since 2021 and the on('message')/send surface has barely moved in years.
Docs4/5A single markdown API doc plus a README with working examples for every common topology; no dedicated docs site or search.
Maintenance5/5Pushed within a day of this review, only 5 open issues, and a long record of prompt releases.
Ecosystem5/5Roughly 249M weekly downloads; it is the substrate under much of Node realtime tooling, including Socket.IO's engine.

Use it if

  • You need a raw WebSocket server in Node.js and want direct control over the protocol
  • You are attaching realtime endpoints to an existing http or https server via the upgrade event
  • You want a dependency-free library that passes the Autobahn protocol suite instead of a framework
  • You need a WebSocket client running inside Node to talk to another backend
Skip it if

Setup reality

npm install ws and you are running; there are no required dependencies. The optional bufferutil native addon speeds up frame masking and ships prebuilt binaries for common platforms, but on unusual ones it needs a C++ toolchain. The real work starts above the protocol: reconnection, heartbeats, auth, and rooms are all yours to build. The README hands you the ping/pong keepalive boilerplate because every production deployment ends up needing it.

Patterns

Connect a Node client and exchange messagesconnect-client

import WebSocket from 'ws';

const ws = new WebSocket('wss://example.com/path');

ws.on('error', console.error);
ws.on('open', () => ws.send('hello'));
ws.on('message', (data) => {
  console.log('received: %s', data);
});

message data is a Buffer by default, not a string; call data.toString() before comparing it to text.

Start a standalone WebSocket serversimple-server

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws) => {
  ws.on('error', console.error);
  ws.on('message', (data) => console.log('received: %s', data));
  ws.send('welcome');
});

Always attach an error listener per socket; an unhandled 'error' event crashes the whole Node process.

Share an existing HTTP/S serverattach-http-server

import { createServer } from 'https';
import { readFileSync } from 'fs';
import { WebSocketServer } from 'ws';

const server = createServer({
  cert: readFileSync('/path/to/cert.pem'),
  key: readFileSync('/path/to/key.pem')
});
const wss = new WebSocketServer({ server });

wss.on('connection', (ws) => {
  ws.on('error', console.error);
});

server.listen(8080);

Pass { server }, not { port }, or ws will try to bind its own listener and conflict with yours.

Route upgrades to multiple servers by URL pathroute-by-path

import { createServer } from 'http';
import { WebSocketServer } from 'ws';

const server = createServer();
const wssChat = new WebSocketServer({ noServer: true });
const wssFeed = new WebSocketServer({ noServer: true });

server.on('upgrade', (request, socket, head) => {
  const { pathname } = new URL(request.url, 'wss://base.url');
  if (pathname === '/chat') {
    wssChat.handleUpgrade(request, socket, head, (ws) => {
      wssChat.emit('connection', ws, request);
    });
  } else if (pathname === '/feed') {
    wssFeed.handleUpgrade(request, socket, head, (ws) => {
      wssFeed.emit('connection', ws, request);
    });
  } else {
    socket.destroy();
  }
});

server.listen(8080);

With noServer: true you must call socket.destroy() on unmatched paths or connections hang open forever.

Authenticate clients before completing the handshakeauthenticate-upgrade

import { createServer } from 'http';
import { WebSocketServer } from 'ws';

const server = createServer();
const wss = new WebSocketServer({ noServer: true });

server.on('upgrade', (request, socket, head) => {
  socket.on('error', console.error);
  authenticate(request, (err, client) => {
    if (err || !client) {
      socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
      socket.destroy();
      return;
    }
    wss.handleUpgrade(request, socket, head, (ws) => {
      wss.emit('connection', ws, request, client);
    });
  });
});

server.listen(8080);

Reject with a raw HTTP 401 on the socket; there is no res object during an upgrade, so express middleware cannot help you here.

Broadcast a message to all connected clientsbroadcast

import WebSocket, { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws) => {
  ws.on('error', console.error);
  ws.on('message', (data, isBinary) => {
    wss.clients.forEach((client) => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(data, { binary: isBinary });
      }
    });
  });
});

Forward the isBinary flag when relaying, otherwise text frames get re-sent as binary and browser clients receive Blobs.

Detect and close broken connections with ping/pongheartbeat-keepalive

import { WebSocketServer } from 'ws';

function heartbeat() {
  this.isAlive = true;
}

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws) => {
  ws.isAlive = true;
  ws.on('error', console.error);
  ws.on('pong', heartbeat);
});

const interval = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (ws.isAlive === false) return ws.terminate();
    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

wss.on('close', () => clearInterval(interval));

Use terminate(), not close(), for dead sockets; close() waits politely for a closing handshake that will never come.

Send binary datasend-binary

import WebSocket from 'ws';

const ws = new WebSocket('wss://example.com/path');

ws.on('error', console.error);
ws.on('open', () => {
  const array = new Float32Array(5);
  for (let i = 0; i < array.length; ++i) array[i] = i / 2;
  ws.send(array);
});

TypedArrays, ArrayBuffers, and Buffers are all accepted; the frame is flagged binary automatically.

Get the client IP address (with and without a proxy)get-client-ip

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws, req) => {
  const direct = req.socket.remoteAddress;
  const behindProxy = req.headers['x-forwarded-for']
    ? req.headers['x-forwarded-for'].split(',')[0].trim()
    : direct;
  ws.on('error', console.error);
});

Behind NGINX or a load balancer remoteAddress is the proxy's IP; you need X-Forwarded-For, and you should only trust it from your own proxy.

Use a WebSocket as a Node duplex streamstream-api

import WebSocket, { createWebSocketStream } from 'ws';

const ws = new WebSocket('wss://websocket-echo.com/');
const duplex = createWebSocketStream(ws, { encoding: 'utf8' });

duplex.on('error', console.error);
duplex.pipe(process.stdout);
process.stdin.pipe(duplex);

Handy for piping to files or other streams, but backpressure applies: a slow consumer will pause the socket.

Alternatives

PackageRegistryPick it when
socket.ionpmWhen you want reconnection, rooms, and broadcast built in instead of hand-rolling them on raw sockets
isomorphic-wsnpmWhen the same client code must run in both Node and the browser