mrkeyoor.com_
Sat 19 Sept 15:49 UTC
npmWeb Backendupdated 19 Sept 2026

ws review

ws 8.21.3 is a Node.js implementation of WebSocket clients and servers. It handles HTTP upgrades, text and binary frames, ping and pong, subprotocols, optional per-message compression, payload limits, streams, and local connection sets. It does not add rooms, reconnect logic, acknowledgements, durable delivery, shared presence, or fallback transports. Version 8.21.3 corrects `permessage-deflate` negotiation when a client offers a smaller maximum window than the server configuration allows. The package is CommonJS with an exports map; both `require()` and ESM import worked in our Node 22 sandbox. It contains no TypeScript declarations, and its browser entry deliberately throws because browsers must use their native `WebSocket`.

245.4Mdownloads / wk
Verdict

ws 8.21.3 installed in 0.7 seconds as one 1 MB package with zero direct dependencies and no audit findings in our sandbox, making it a lean Node transport when raw WebSockets are the actual requirement. Choose Socket.IO when the product needs rooms, reconnects, acknowledgements, and cross-process adapters rather than protocol primitives.

We installed it

Lab card: what happened when we installed wsScreenshot of ws documentation
Install✓ · 0.7s1 package on disk · 1 MB
ImportESM import works · require() works · CommonJS package with exports map
Browser0.4 KBgzipped (0.7 KB minified), bundled with esbuild
Typesno TypeScript types found
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does ws install cleanly?

Yes. In a fresh container with an empty cache, npm install ws finished in 0.7s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.

How much does ws add to a browser bundle?

0.4 KB gzipped (0.7 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does ws work with both ESM and CommonJS?

Yes. Both import 'ws' and require('ws') worked in Node 22 in our run. The package is published as CommonJS with an exports map.

Does ws include TypeScript types?

No type declarations were found in our install, so TypeScript users need their own declarations.

ws or socket.io: which should you use?

socket.io: Use it for rooms, acknowledgements, reconnect behavior, adapters, and transport fallback above raw WebSockets. ws 8.21.3 installed in 0.7 seconds as one 1 MB package with zero direct dependencies and no audit findings in our sandbox, making it a lean Node transport when raw WebSockets are the actual requirement.

When should you not use ws?

The code runs in a browser. The ws README says to use the platform WebSocket; this package's browser export is an error stub.

API stability5/5The version 8 API continues to revolve around `WebSocket`, `WebSocketServer`, Node-style events, `send()`, `ping()`, `terminate()`, streams, and explicit upgrade handling. Version 8.21.3 changes only compression offer validation, leaving application message formats untouched because ws never owns them. Runtime changes, optional native modules, reverse proxies, and zlib settings can still alter behavior under load, so protocol stability does not remove the need for deployment tests.
Docs4/5The README includes standalone and shared HTTP servers, multiple upgrade paths, authentication, broadcast loops, heartbeat detection, IP forwarding, compression controls, streams, client examples, and optional addons. A separate `doc/ws.md` covers constructor options and events in Node's reference style. There is no dedicated searchable docs site, and production policies for backpressure, Origin checks, shutdown ordering, brokers, and authentication deadlines must be assembled from examples plus Node infrastructure knowledge.
Maintenance5/5Version 8.21.3 was published on August 7, 2026, and the repository was pushed on August 13. GitHub reports 22,793 stars and only five open issues plus pull requests. The latest release contains one precise compression-negotiation fix, while supported older major lines also received a fragment-counter repair in July. The project runs the Autobahn protocol suite and keeps the mandatory dependency count at zero.
Ecosystem5/5The official npm endpoint counted 266,459,247 downloads for August 19 through August 25, 2026. ws is used directly by Node services and underneath frameworks, SDKs, test tools, and development servers; its events and streams fit the rest of Node. That reach also produces misleading snippets that broadcast without pressure limits or omit heartbeats, and browser developers must understand that the npm package is not a frontend WebSocket polyfill.

Use it if

  • A Node service needs raw WebSocket connections and your application will define messages, authorization, retries, and fan-out.
  • Several WebSocket paths must share an existing HTTP or HTTPS listener through explicit upgrade routing.
  • A backend WebSocket client needs Node TLS, headers, proxy agents, or duplex-stream integration unavailable to browser code.
  • You need protocol-level settings and are prepared to implement heartbeats, slow-client handling, size limits, and shutdown.
Skip it if

Setup reality

We installed ws 8.21.3 in a fresh unprivileged Node 22 Bookworm sandbox. npm completed in 0.7 seconds and left one package using 1 MB. The package has zero direct and two peer dependencies, measures 204 KB unpacked, requires Node 10+, and uses the MIT license. npm audit found zero known vulnerabilities at critical, high, moderate, and low severity. CommonJS require() and ESM import both succeeded through the exports map.

Our browser build produced 0.7 KB minified and 0.4 KB gzipped, but that output is only the package's throwing browser stub. Frontend code must use globalThis.WebSocket. No TypeScript declarations were present in our install, so TypeScript users need external ws types. Optional peer packages can accelerate masking or legacy UTF-8 checking; the basic install works without either native addon.

With noServer: true, your HTTP upgrade handler must validate path, Origin, cookies, and credentials before calling handleUpgrade() exactly once. Reject and destroy invalid sockets, and put a deadline around asynchronous authentication. Express response middleware does not protect an upgrade because there is no normal response. For browser clients, compare Origin against an allowlist instead of accepting any nonempty value.

Set maxPayload, handle errors from the send() callback, and cap bufferedAmount before a slow connection accumulates unlimited queued data. Ping clients on an interval and terminate those that miss pong. During shutdown, stop upgrades, send close code 1001, wait for a deadline, then terminate stragglers. WebSocketServer.clients covers one process only; multi-instance broadcast and presence need an external broker plus application-level ownership rules.

Patterns

Start a standalone server with a payload cap start-bounded-server

import { WebSocketServer } from "ws";

const wss = new WebSocketServer({ port: 8080, maxPayload: 1024 * 1024 });
wss.on("connection", socket => {
  socket.on("error", console.error);
  socket.send(JSON.stringify({ type: "ready" }));
});

Attach an error handler to every accepted socket. Set `maxPayload` from the largest valid protocol message rather than leaving size policy implicit.

Authenticate before switching protocols authenticate-upgrade

server.on("upgrade", async (request, socket, head) => {
  try {
    const user = await authenticate(request);
    wss.handleUpgrade(request, socket, head, ws => {
      wss.emit("connection", ws, request, user);
    });
  } catch {
    socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
    socket.destroy();
  }
});

Put a deadline around authentication and check Origin for browser clients. Upgrade handling has a raw socket instead of the usual HTTP response object.

Send one URL path to a no-server instance route-upgrade-path

const wss = new WebSocketServer({ noServer: true });
server.on("upgrade", (request, socket, head) => {
  const url = new URL(request.url, "http://localhost");
  if (url.pathname !== "/events") return socket.destroy();
  wss.handleUpgrade(request, socket, head, ws => {
    wss.emit("connection", ws, request);
  });
});

Call `handleUpgrade()` no more than once for a socket. Destroy unmatched connections so they do not remain open without an owner.

Separate binary frames from JSON text parse-text-frame

socket.on("message", (data, isBinary) => {
  if (isBinary) return handleBinary(data);
  const message = JSON.parse(data.toString("utf8"));
  handleMessage(message);
});

A Node text frame still arrives as Buffer-like data. Validate the parsed object and apply message-specific limits before dispatch.

Evict broadcast clients with excessive queued bytes broadcast-with-queue-cap

import WebSocket from "ws";

for (const client of wss.clients) {
  if (client.readyState !== WebSocket.OPEN) continue;
  if (client.bufferedAmount > 1024 * 1024) {
    client.terminate();
    continue;
  }
  client.send(payload, error => { if (error) client.terminate(); });
}

Choose the 1 MB threshold from message rate and tolerated latency. Large systems usually need individual queues and metrics instead of one broadcast loop.

Terminate connections that miss pong heartbeat-clients

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

wss.on("connection", socket => {
  socket.isAlive = true;
  socket.on("pong", heartbeat);
});
const timer = setInterval(() => {
  for (const socket of wss.clients) {
    if (!socket.isAlive) { socket.terminate(); continue; }
    socket.isAlive = false;
    socket.ping();
  }
}, 30_000);

Clear the interval during shutdown. After a missed pong, `terminate()` avoids waiting for a closing handshake from a peer that may be unreachable.

Send a subscription after the Node client opens connect-node-client

import WebSocket from "ws";

const socket = new WebSocket("wss://events.example/socket");
socket.on("open", () => {
  socket.send(JSON.stringify({ type: "subscribe", topic: "orders" }));
});
socket.on("message", data => console.log(data.toString()));
socket.on("error", console.error);

ws does not reconnect. Add bounded exponential backoff with jitter and decide what happens to messages created while disconnected.

Expose a socket through Node stream backpressure create-duplex-stream

import { createWebSocketStream } from "ws";

const duplex = createWebSocketStream(socket, { encoding: "utf8" });
source.pipe(duplex);
duplex.pipe(destination);
duplex.on("error", console.error);

The wrapper participates in Node stream pressure. Define whether stream end closes the WebSocket and how a remote close affects each pipeline.

Close clients, then terminate stragglers shutdown-with-deadline

wss.close();
for (const socket of wss.clients) socket.close(1001, "server shutdown");
const deadline = setTimeout(() => {
  for (const socket of wss.clients) socket.terminate();
}, 5_000);
wss.once("close", () => clearTimeout(deadline));

Also stop the HTTP server from accepting new upgrades. The 5-second deadline prevents shutdown from waiting forever on clients that ignore close frames.

Keep server compression disabled disable-message-compression

const wss = new WebSocketServer({
  server,
  perMessageDeflate: false,
  maxPayload: 1024 * 1024,
});

Server compression is already off by default. Enable it only after representative tests show worthwhile bandwidth savings and acceptable zlib memory use.

Alternatives

PackageRegistryPick it when
socket.ionpmUse it for rooms, acknowledgements, reconnect behavior, adapters, and transport fallback above raw WebSockets.
isomorphic-wsnpmUse it when shared client source must select ws on Node and native WebSocket in a browser.
websocketnpmUse it only for an existing project already built around that package's distinct API after checking current maintenance.

More web backend guides

urllib3 · requests · anyio · undici · httpx · express · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.