# WebSocket Client

> A per-runtime WebSocket client for dialing upstream servers with one consistent signature.

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](/guide/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.

<note>

`crossws/websocket` is a **client** (it dials out). To accept incoming connections, use an [adapter](/adapters) or [`crossws/server`](/guide).

</note>

## Usage

Import the default export and construct it like a standard `WebSocket`:

```ts
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`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) — the same events (`open`, `message`, `close`, `error`), methods (`send`, `close`), and static members (`WebSocket.OPEN`, …) you already know.

## Signature

```ts
new WebSocket(url, protocols?, options?);
```

- **url** — `string | URL`. The upstream to dial: `ws:`, `wss:`, or a [`ws+unix://<socketPath>:<pathname>`](#unix-domain-sockets) 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 upgrade `headers`. 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:

```ts
import WebSocket from "crossws/websocket";

const ws = new WebSocket("wss://backend.example.com", undefined, {
  headers: { authorization: "Bearer …" },
});
```

<tip>

When you call it with just `(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.

</tip>

## Per-runtime behavior

crossws resolves a runtime-specific implementation through [export conditions](https://nodejs.org/api/packages.html#conditional-exports), so a bundle built for one runtime tree-shakes away the others (e.g. `ws` and `node:*` never enter a Deno or browser bundle):

<table>
<thead>
  <tr>
    <th>
      Runtime
    </th>
    
    <th>
      Implementation
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Bun
      </strong>
    </td>
    
    <td>
      Global <code>
        WebSocket
      </code>
      
       — dials <code>
        ws:
      </code>
      
      /<code>
        wss:
      </code>
      
      /<code>
        ws+unix:
      </code>
      
       natively. crossws relays <code>
        options
      </code>
      
       into Bun's second-argument form.
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Node.js
      </strong>
    </td>
    
    <td>
      Global <code>
        WebSocket
      </code>
      
       (undici) for the common <code>
        ws:
      </code>
      
      /<code>
        wss:
      </code>
      
       path; routes through <a href="https://github.com/websockets/ws" rel="nofollow">
        <code>
          ws
        </code>
      </a>
      
       (bundled, no extra dependency) whenever <code>
        options
      </code>
      
       are passed or the target is <code>
        ws+unix:
      </code>
      
       — and everywhere on Node < 22, which has no global.
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Deno
      </strong>
    </td>
    
    <td>
      Global <code>
        WebSocket
      </code>
      
      ; relays <code>
        options
      </code>
      
       into Deno's second-argument form, and rewrites <code>
        ws+unix:
      </code>
      
       targets through <a href="https://docs.deno.com/api/deno/~/Deno.createHttpClient" rel="nofollow">
        <code>
          Deno.createHttpClient
        </code>
      </a>
      
      's unix <code>
        client
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Browser / Workers / edge
      </strong>
    </td>
    
    <td>
      The WHATWG global <code>
        WebSocket
      </code>
      
       verbatim. It has no third-argument options, so custom <code>
        headers
      </code>
      
       are silently ignored.
    </td>
  </tr>
</tbody>
</table>

<important>

Custom `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.

</important>

## 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:

```ts
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`).

<note>

On **Deno**, the unix transport uses `Deno.createHttpClient`, an **unstable** API — run Deno with the `--unstable-net` flag to enable it.

</note>

## Relationship to the proxy

[`createWebSocketProxy()`](/guide/proxy) dials every upstream through this client, which is why its [`headers`](/guide/proxy#forwarding-headers), [`webSocketOptions`](/guide/proxy#per-peer-constructor-options), and [`ws+unix:` targets](/guide/proxy#unix-domain-sockets) work uniformly across runtimes without a custom constructor. Passing an explicit [`WebSocket` option](/guide/proxy#custom-websocket-constructor) to the proxy opts out of this per-runtime handling and dials with your constructor verbatim.

<read-more title="WebSocket Proxy" to="/guide/proxy">

See the [WebSocket Proxy](/guide/proxy) guide for forwarding incoming connections to an upstream.

</read-more>
