WebSocket Client
Every runtime exposes a slightly different WebSocket constructor for dialing a server: Bun and Deno take their dialing options as the constructor's second argument, the WHATWG/ws clients take a third, and only some accept the ws+unix: scheme or custom upgrade headers. crossws smooths this over with the crossws/websocket subpath — a thin per-runtime client that dials ws:, wss:, and ws+unix: upstreams through one uniform (url, protocols, options) signature.
This is the same client the proxy uses under the hood. Reach for it directly when you need to dial an upstream WebSocket yourself — for example from a custom hook — and want custom headers or Unix-socket support to work the same way on every runtime.
crossws/websocket is a client (it dials out). To accept incoming connections, use an adapter or crossws/server.Usage
Import the default export and construct it like a standard WebSocket:
import WebSocket from "crossws/websocket";
const ws = new WebSocket("wss://echo.websocket.org");
ws.onopen = () => ws.send("hello");
ws.onmessage = (event) => console.log(event.data);
The returned instance is a standard WebSocket — the same events (open, message, close, error), methods (send, close), and static members (WebSocket.OPEN, …) you already know.
Signature
new WebSocket(url, protocols?, options?);
url—string | URL. The upstream to dial:ws:,wss:, or aws+unix://<socketPath>:<pathname>target.protocols—string | string[](optional). Subprotocol(s) offered during the handshake, exactly as the WHATWG constructor.options—Record<string, unknown>(optional). Extra dialing options — most commonly custom upgradeheaders. crossws relays this into whichever argument position the current runtime expects.
The third options argument is the value the WHATWG browser constructor can't express. crossws makes it behave the same everywhere:
import WebSocket from "crossws/websocket";
const ws = new WebSocket("wss://backend.example.com", undefined, {
headers: { authorization: "Bearer …" },
});
(url, protocols) and no options, crossws keeps the runtime's native positional path — so option-less calls are identical to using the global WebSocket directly.Per-runtime behavior
crossws resolves a runtime-specific implementation through export conditions, so a bundle built for one runtime tree-shakes away the others (e.g. ws and node:* never enter a Deno or browser bundle):
| Runtime | Implementation |
|---|---|
| Bun | Global WebSocket — dials ws:/wss:/ws+unix: natively. crossws relays options into Bun's second-argument form. |
| Node.js | Global WebSocket (undici) for the common ws:/wss: path; routes through ws (bundled, no extra dependency) whenever options are passed or the target is ws+unix: — and everywhere on Node < 22, which has no global. |
| Deno | Global WebSocket; relays options into Deno's second-argument form, and rewrites ws+unix: targets through Deno.createHttpClient's unix client. |
| Browser / Workers / edge | The WHATWG global WebSocket verbatim. It has no third-argument options, so custom headers are silently ignored. |
headers (and other options) are honored on Bun, Deno, and Node — not in browsers, Cloudflare Workers, or other edge runtimes, whose WHATWG constructor drops them. Don't rely on upstream identity via headers there.Unix domain sockets
Dial an upstream listening on a Unix domain socket by passing a ws+unix://<socketPath>:<pathname> URL. This works out of the box on Node.js, Bun, and Deno:
import WebSocket from "crossws/websocket";
const ws = new WebSocket("ws+unix:///var/run/backend.sock:/chat");
The path before the : is the socket file (/var/run/backend.sock); the path after it is the request path forwarded to the upstream (/chat).
Deno.createHttpClient, an unstable API — run Deno with the --unstable-net flag to enable it.Relationship to the proxy
createWebSocketProxy() dials every upstream through this client, which is why its headers, webSocketOptions, and ws+unix: targets work uniformly across runtimes without a custom constructor. Passing an explicit WebSocket option to the proxy opts out of this per-runtime handling and dials with your constructor verbatim.