cheat sheet

ws

Package-level reference for ws on npm — client/server, broadcast, heartbeat, compression, and security pitfalls.

ws

What it is

ws is the canonical Node.js WebSocket implementation — RFC 6455 compliant, the fastest pure-JS WS library, used by Socket.IO, Apollo Server (subscriptions), nodemon, and most server-side WebSocket code on Node. It exposes both WebSocketServer (server) and WebSocket (client) classes, with a browser-compatible client API.

It's tiny (~50 KB unpacked), zero-runtime-dependencies (optional bufferutil and utf-8-validate for native-code speedups), and ships every WS feature: compression (permessage-deflate), fragmentation, control frames (ping/pong/close), per-connection backpressure, and HTTP upgrade integration with http, https, http2, Fastify, Express, and Koa.

In 2026, native WebSocket support landed in Node ≥22 as part of the URL/Fetch standard alignment. But ws remains the production choice — broader feature surface, mature, and 2-3× faster than the built-in.

Install

bash
# npm / pnpm / yarn / bun
npm install ws
pnpm add ws
yarn add ws
bun add ws

Output: runtime dep. ~50 KB unpacked, no runtime dependencies.

bash
# TypeScript declarations are separate
npm install --save-dev @types/ws

Output: DefinitelyTyped declarations. Required for any TS project using ws.

bash
# Optional native-code accelerators (recommended)
npm install bufferutil utf-8-validate

Output: C++ addons for masking/unmasking and UTF-8 validation — 2-3× throughput improvement. Optional; ws falls back to pure-JS.

Versioning & Node support

  • Current major line is 8.x (stable since 2021; minors continue). API surface unchanged for years; the line absorbs RFC 8441 (WebSockets over HTTP/2), incremental perf, and CVE patches.
  • Node ≥10 required (recommend 18+). Newer minors track Node LTS — ws@8.18 runs on Node 18, 20, 22.
  • Pure JS with optional native addons.
  • CJS-first, but ESM-importable via the dual-export shim. import WebSocket from "ws" works in both ESM and CJS.
  • Always a runtime dependency.
  • Strict semver — last major (8.x) added permessage-deflate fragment fixes and removed the deprecated verifyClient legacy callback shape.

Package metadata

  • Maintainer: Luigi Pinca (@lpinca) + co-maintainers
  • Project home: github.com/websockets/ws
  • Docs: github.com/websockets/ws/blob/master/doc/ws.md
  • npm: npmjs.com/package/ws
  • License: MIT
  • First released: 2011
  • Downloads: ~100 million per week — top-15 package on npm

Peer dependencies & extras

Zero runtime dependencies. Optional native addons:

PackagePurpose
bufferutilNative masking/unmasking; 2-3× faster than pure-JS.
utf-8-validateNative UTF-8 validation for text frames.
@types/wsTS declarations.
socket.ioHigher-level WS framework — uses ws under the hood, adds rooms, namespaces, fallback, ack-based RPC.
engine.ioLower-level transport layer behind Socket.IO.
graphql-wsGraphQL subscriptions over WS — uses ws under the hood.
subscriptions-transport-wsOlder Apollo WS protocol — being replaced by graphql-ws.

Alternatives

LibraryTrade-off
Node ≥22 built-in WebSocketFree, zero-dep, client only. Server-side coming. Pick for client-side scripts on modern Node.
socket.ioAdds rooms, namespaces, ack-based RPC, transport fallback to long-polling. ~10× larger. Pick when you want batteries included.
graphql-wsGraphQL subscriptions only. Built on ws. Pick when GraphQL is the wire format.
uWebSockets.jsNative C++ WS server. 5-10× faster than ws. Pick for tens of thousands of concurrent connections. Steeper integration.
fastify-websocketFastify plugin built on ws. Pick for Fastify apps that want native WS routing.
partyserver / cloudflare workersEdge-runtime WS via the Cloudflare Workers API. Pick for serverless WS at the edge.

Common gotchas

  1. The WebSocket class IS browser-compatible. Client code that uses new WebSocket(url) works identically with ws and the browser global — Universal modules just import WebSocket from "ws" server-side and use the browser global client-side.
  2. WebSocketServer doesn't bind a port on its own when given an existing HTTP server. new WebSocketServer({ server }) reuses an existing HTTP server's upgrade event; ws does not call .listen().
  3. message arrives as a Buffer by default, not string. Set binaryType: "arraybuffer" or coerce explicitly. Many bugs come from concatenating raw Buffers with +.
  4. No automatic reconnection. ws is a thin protocol layer; reconnection logic is your responsibility. Common patterns are exponential backoff + jitter, or a wrapper like reconnecting-websocket.
  5. Backpressure isn't auto-handled. socket.send(largeBuf) queues; if the peer is slow, memory grows unbounded. Check socket.bufferedAmount before sending, or use socket._socket.write semantics.
  6. pong frames are received automatically but ping isn't sent automatically. You must implement your own heartbeat for dead-connection detection.

Real-world recipes

Client — connect and exchange messages

typescript
import WebSocket from "ws";

const ws = new WebSocket("wss://api.example.com/socket");

ws.on("open", () => {
  console.log("connected");
  ws.send(JSON.stringify({ type: "hello", name: "alice" }));
});

ws.on("message", (data) => {
  const msg = JSON.parse(data.toString());
  console.log("got:", msg);
});

ws.on("close", (code, reason) => {
  console.log("closed", code, reason.toString());
});

ws.on("error", (err) => {
  console.error("ws error:", err);
});

Output:

text
connected
got: { type: "welcome", session: "abc123" }
got: { type: "pong" }
closed 1000 normal

data is a Buffer (or ArrayBuffer / Blob depending on binaryType). Always coerce to the expected type.

Server — upgrade an HTTP server

typescript
import http from "node:http";
import { WebSocketServer } from "ws";

const server = http.createServer((req, res) => {
  res.writeHead(200);
  res.end("HTTP server, WS at /socket");
});

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

server.on("upgrade", (req, socket, head) => {
  if (req.url === "/socket") {
    wss.handleUpgrade(req, socket, head, (ws) => {
      wss.emit("connection", ws, req);
    });
  } else {
    socket.destroy();
  }
});

wss.on("connection", (ws, req) => {
  console.log("client from", req.socket.remoteAddress);
  ws.on("message", (data) => ws.send(`echo: ${data}`));
});

server.listen(3000);

Output: HTTP and WS on the same port; multiple WS endpoints possible by URL routing in the upgrade handler.

Broadcast to all clients

typescript
import { WebSocketServer, WebSocket } from "ws";

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

wss.on("connection", (ws) => {
  ws.on("message", (data, isBinary) => {
    for (const client of wss.clients) {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(data, { binary: isBinary });
      }
    }
  });
});

Output: sender excluded by client !== ws. client.readyState === OPEN check avoids not open errors on connecting/closing peers. For large rooms, broadcast cost is O(N) per message — use socket.io rooms for sub-grouping or uWebSockets.js for very high fan-out.

Heartbeat — detect dead connections

typescript
import { WebSocketServer } from "ws";

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

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

wss.on("connection", (ws) => {
  (ws as any).isAlive = true;
  ws.on("pong", heartbeat);
});

const interval = setInterval(() => {
  for (const ws of wss.clients) {
    if ((ws as any).isAlive === false) return ws.terminate();
    (ws as any).isAlive = false;
    ws.ping();
  }
}, 30_000);

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

Output: every 30s, send ping to each client. If they didn't pong since the last cycle, kill the connection. Critical for any production WS — TCP can quietly hold dead connections for minutes.

Compression (permessage-deflate)

typescript
import { WebSocketServer } from "ws";

const wss = new WebSocketServer({
  port: 8080,
  perMessageDeflate: {
    zlibDeflateOptions: { level: 3, chunkSize: 1024 },
    zlibInflateOptions: { chunkSize: 10 * 1024 },
    clientNoContextTakeover: true,
    serverNoContextTakeover: true,
    serverMaxWindowBits: 10,
    concurrencyLimit: 10,
    threshold: 1024,
  },
});

Output: enables RFC 7692 permessage-deflate compression. Reduces JSON/text traffic by 60-90%. Trade-off: CPU per message ≈ 30 µs server-side; rarely worth it for tiny messages (the threshold: 1024 skips messages smaller than 1KB).

Security note: permessage-deflate historically had several DoS-amplification CVEs; the patched defaults are safe but disable it (perMessageDeflate: false) for very high message-rate servers.

Authenticated upgrade

typescript
import http from "node:http";
import { WebSocketServer } from "ws";
import jwt from "jsonwebtoken";

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

const server = http.createServer();
server.on("upgrade", async (req, socket, head) => {
  const token = new URL(req.url!, "ws://x").searchParams.get("token");
  try {
    const payload = jwt.verify(token!, process.env.JWT_SECRET!);
    wss.handleUpgrade(req, socket, head, (ws) => {
      (ws as any).user = payload;
      wss.emit("connection", ws, req);
    });
  } catch {
    socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
    socket.destroy();
  }
});

server.listen(3000);

Output: JWT-verified upgrade. Reject before consuming the upgrade — attackers can't tie up server resources on bad tokens. Per-message auth is also viable but the upgrade-time check is more efficient.

Production deployment

Pair with a process manager

WS servers must survive restarts gracefully. Use pm2 or systemd:

javascript
// pm2 ecosystem.config.js
module.exports = {
  apps: [{
    name: "ws-server",
    script: "./dist/server.js",
    instances: 4,
    exec_mode: "cluster",
    listen_timeout: 10000,
    kill_timeout: 30000,    // give clients time to reconnect
  }],
};

WS sessions are stateful — when restarting, clients must reconnect. Add a reconnect-with-backoff on the client side.

Sticky sessions for multi-process

If you cluster, clients must hit the same process every request (cookies, IP-based stickiness, or a shared session store). Otherwise the WS upgrade lands on process A but stateful follow-ups land on process B. Use redis to share state, or sticky-session middleware.

Behind a reverse proxy

Nginx, Caddy, Cloudflare — all require explicit Upgrade: websocket proxying:

nginx
location /socket {
  proxy_pass http://backend:8080;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
  proxy_read_timeout 86400;
}

The 86400s read-timeout matters — defaults are often 60s, killing idle WS connections.

Edge runtime compatibility

ws is Node-only. For Cloudflare Workers, Vercel Edge, Bun, Deno — use the native WebSocket API. The protocols are compatible, but the server-side library doesn't run.

Performance tuning

Install the native addons

bash
npm install bufferutil utf-8-validate

Output:

text
added 2 packages in 2s

ws auto-detects and uses them. 2-3× throughput on message-heavy workloads.

Disable compression for tiny messages

typescript
new WebSocketServer({ port: 8080, perMessageDeflate: false });

For sub-1KB messages, compression cost > savings. Disable explicitly.

Use socket.send(buf, { binary: true }) for binary data

Auto-detection has overhead. Be explicit.

Backpressure check

typescript
function safeSend(ws, data) {
  if (ws.bufferedAmount > 1024 * 1024) {
    // 1MB queued — slow consumer
    return false;
  }
  ws.send(data);
  return true;
}

Without this, a slow client can balloon Node memory. Drop messages or close the connection beyond a threshold.

Use uWebSockets.js for extreme scale

For >10k concurrent connections per process, uWebSockets.js is 5-10× faster. ws is fine up to a few thousand connections per process.

Version migration guide

ws 8.x has been stable since 2021. Migration history:

v7 → v8

  • Node ≥10 required (newer minors require 18+).
  • verifyClient callback shape simplified. Use (info, cb) => cb(true/false) only — older 3-arg shape removed.
  • Default maxPayload is 100MB. Lower it for untrusted clients (maxPayload: 1024 * 1024).
  • handleProtocols callback shape unified.
  • permessage-deflate defaults safer. Several DoS CVEs patched.

v8.x internal

  • 8.5+ added RFC 8441 — WebSockets over HTTP/2.
  • 8.13+ patched a permessage-deflate slowloris-style DoS (CVE-2024-37890).
  • 8.17+ patched another integer-overflow in the masking path.

Stay on ^8.18 for the current CVE-clean version.

v8.x → "v9"

Not yet announced. Likely will be ESM-first and require Node 22+.

Security considerations

WS is a long-lived persistent connection — every CVE has been about exhausting server resources via malformed frames.

CVEYearNotes
CVE-2021-326402021ReDoS in Sec-WebSocket-Protocol header parsing
CVE-2024-378902024permessage-deflate slowloris DoS — fixed in 8.17.1
CVE-2024-??2024Integer overflow in masking — fixed in 8.18.0

Rules:

  1. Pin to ws@^8.18 (or the latest 8.x). Older versions are exploitable.
  2. Set maxPayload to the smallest size your app needs. Default 100MB is far too generous for chat-style traffic.
  3. Authenticate at upgrade-time. Don't let unauthed clients open WS connections — they hold resources.
  4. Implement heartbeats and timeouts. Without them, a malicious client can hold connections open indefinitely (slowloris-style DoS).
  5. Rate-limit per IP / per connection. A single bad client can overwhelm a server. Apply both connection-rate and message-rate limits.
  6. Origin check. Set origin checks in the upgrade handler to reject cross-site WS attacks (CSWSH). ws doesn't enforce by default.
  7. Disable permessage-deflate for untrusted peers if you don't need it — it has historically been the DoS attack surface.

Testing & CI integration

typescript
import { describe, it, expect } from "vitest";
import { WebSocketServer, WebSocket } from "ws";

describe("echo server", () => {
  it("echoes a message", async () => {
    const wss = new WebSocketServer({ port: 0 });
    wss.on("connection", (ws) => ws.on("message", (m) => ws.send(m)));

    const { port } = wss.address() as any;
    const client = new WebSocket(`ws://localhost:${port}`);

    const reply: any = await new Promise((res) => {
      client.on("open", () => client.send("hello"));
      client.on("message", res);
    });

    expect(reply.toString()).toBe("hello");
    client.close();
    wss.close();
  });
});

Output: port: 0 makes the OS pick a free port — avoids test conflicts. Always .close() server and client; otherwise Vitest hangs.

For load tests, use wscat or artillery:

bash
npm install -g wscat
wscat -c ws://localhost:8080

Output:

text
Connected (press CTRL+C to quit)
> hello
< echo: hello

Ecosystem integrations

ToolIntegration
expressws doesn't integrate directly — use express-ws (wraps ws) or manual upgrade
fastify@fastify/websocket plugin — built on ws
koaManual upgrade handling, similar to plain Node http
next.jsApp Router doesn't support raw WS — use Pages API with manual upgrade, or use Socket.IO
socket.ioBuilt on ws
graphql-wsGraphQL subscriptions over ws
pinoLog WS events via pino transport
helmetHTTP security headers — doesn't affect WS frames

Troubleshooting common errors

  • Error: not opened — sending on a WebSocket not yet open. Wait for open event, or check readyState === WebSocket.OPEN.
  • Error: WebSocket was closed before the connection was established — closing during handshake. Usually a server-side reject or network blip.
  • Invalid frame header — protocol violation by the peer. Often misconfigured proxy/load-balancer.
  • Maximum payload size exceeded — peer sent a frame larger than maxPayload. Increase the limit or reject the peer.
  • Memory growth over time — backpressure not handled, or no heartbeat. Check bufferedAmount and implement ping/pong.
  • Connections die after 60s — proxy idle timeout. Increase proxy_read_timeout in nginx, or send periodic keepalive frames.
  • TS error Cannot find name 'WebSocket' — install @types/ws and import WebSocket from "ws".

When NOT to use this

  • You need cross-runtime (Workers, Edge, Bun, Deno). Use the platform WebSocket API. ws is Node-only.
  • You want rooms / namespaces / acks. Use Socket.IO — built on ws but adds a lot of structure.
  • You need >10k concurrent connections per process. Use uWebSockets.js — 5-10× faster.
  • You're doing GraphQL subscriptions. Use graphql-ws — handles the GraphQL protocol on top of ws for you.
  • You need long-polling fallback for old clients. Use Socket.IO — has automatic transport fallback.
  • You just need a simple echo / one-way push. Server-Sent Events (SSE) — text/event-stream — is simpler, one-way, and works through more firewalls.

See also