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
# 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.
# TypeScript declarations are separate
npm install --save-dev @types/ws
Output: DefinitelyTyped declarations. Required for any TS project using ws.
# 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.18runs 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 deprecatedverifyClientlegacy 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:
| Package | Purpose |
|---|---|
bufferutil | Native masking/unmasking; 2-3× faster than pure-JS. |
utf-8-validate | Native UTF-8 validation for text frames. |
@types/ws | TS declarations. |
socket.io | Higher-level WS framework — uses ws under the hood, adds rooms, namespaces, fallback, ack-based RPC. |
engine.io | Lower-level transport layer behind Socket.IO. |
graphql-ws | GraphQL subscriptions over WS — uses ws under the hood. |
subscriptions-transport-ws | Older Apollo WS protocol — being replaced by graphql-ws. |
Alternatives
| Library | Trade-off |
|---|---|
Node ≥22 built-in WebSocket | Free, zero-dep, client only. Server-side coming. Pick for client-side scripts on modern Node. |
| socket.io | Adds rooms, namespaces, ack-based RPC, transport fallback to long-polling. ~10× larger. Pick when you want batteries included. |
| graphql-ws | GraphQL subscriptions only. Built on ws. Pick when GraphQL is the wire format. |
| uWebSockets.js | Native C++ WS server. 5-10× faster than ws. Pick for tens of thousands of concurrent connections. Steeper integration. |
| fastify-websocket | Fastify plugin built on ws. Pick for Fastify apps that want native WS routing. |
| partyserver / cloudflare workers | Edge-runtime WS via the Cloudflare Workers API. Pick for serverless WS at the edge. |
Common gotchas
- The
WebSocketclass IS browser-compatible. Client code that usesnew WebSocket(url)works identically withwsand the browser global — Universal modules justimport WebSocket from "ws"server-side and use the browser global client-side. WebSocketServerdoesn'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().messagearrives as aBufferby default, not string. SetbinaryType: "arraybuffer"or coerce explicitly. Many bugs come from concatenating raw Buffers with+.- No automatic reconnection.
wsis a thin protocol layer; reconnection logic is your responsibility. Common patterns are exponential backoff + jitter, or a wrapper likereconnecting-websocket. - Backpressure isn't auto-handled.
socket.send(largeBuf)queues; if the peer is slow, memory grows unbounded. Checksocket.bufferedAmountbefore sending, or usesocket._socket.writesemantics. pongframes are received automatically butpingisn't sent automatically. You must implement your own heartbeat for dead-connection detection.
Real-world recipes
Client — connect and exchange messages
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:
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
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
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
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)
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
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:
// 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:
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
npm install bufferutil utf-8-validate
Output:
added 2 packages in 2s
ws auto-detects and uses them. 2-3× throughput on message-heavy workloads.
Disable compression for tiny messages
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
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+).
verifyClientcallback shape simplified. Use(info, cb) => cb(true/false)only — older 3-arg shape removed.- Default
maxPayloadis 100MB. Lower it for untrusted clients (maxPayload: 1024 * 1024). handleProtocolscallback 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.
| CVE | Year | Notes |
|---|---|---|
| CVE-2021-32640 | 2021 | ReDoS in Sec-WebSocket-Protocol header parsing |
| CVE-2024-37890 | 2024 | permessage-deflate slowloris DoS — fixed in 8.17.1 |
| CVE-2024-?? | 2024 | Integer overflow in masking — fixed in 8.18.0 |
Rules:
- Pin to
ws@^8.18(or the latest 8.x). Older versions are exploitable. - Set
maxPayloadto the smallest size your app needs. Default 100MB is far too generous for chat-style traffic. - Authenticate at upgrade-time. Don't let unauthed clients open WS connections — they hold resources.
- Implement heartbeats and timeouts. Without them, a malicious client can hold connections open indefinitely (slowloris-style DoS).
- Rate-limit per IP / per connection. A single bad client can overwhelm a server. Apply both connection-rate and message-rate limits.
- Origin check. Set
originchecks in the upgrade handler to reject cross-site WS attacks (CSWSH).wsdoesn't enforce by default. - Disable
permessage-deflatefor untrusted peers if you don't need it — it has historically been the DoS attack surface.
Testing & CI integration
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:
npm install -g wscat
wscat -c ws://localhost:8080
Output:
Connected (press CTRL+C to quit)
> hello
< echo: hello
Ecosystem integrations
| Tool | Integration |
|---|---|
express | ws doesn't integrate directly — use express-ws (wraps ws) or manual upgrade |
fastify | @fastify/websocket plugin — built on ws |
koa | Manual upgrade handling, similar to plain Node http |
next.js | App Router doesn't support raw WS — use Pages API with manual upgrade, or use Socket.IO |
socket.io | Built on ws |
graphql-ws | GraphQL subscriptions over ws |
pino | Log WS events via pino transport |
helmet | HTTP security headers — doesn't affect WS frames |
Troubleshooting common errors
Error: not opened— sending on aWebSocketnot yet open. Wait foropenevent, or checkreadyState === 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 thanmaxPayload. Increase the limit or reject the peer.- Memory growth over time — backpressure not handled, or no heartbeat. Check
bufferedAmountand implement ping/pong. - Connections die after 60s — proxy idle timeout. Increase
proxy_read_timeoutin nginx, or send periodic keepalive frames. - TS error
Cannot find name 'WebSocket'— install@types/wsandimport WebSocket from "ws".
When NOT to use this
- You need cross-runtime (Workers, Edge, Bun, Deno). Use the platform
WebSocketAPI.wsis 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
- Concept: HTTP — Upgrade header and WS handshake
- Concept: async — event-driven message handling
- JavaScript: node runtime — http upgrade events