> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/oven-sh/bun/llms.txt
> Use this file to discover all available pages before exploring further.

# WebSockets

> Build WebSocket servers with Bun.serve(). Includes pub/sub, compression, contextual data, and a native WebSocket client.

`Bun.serve()` has first-class support for server-side WebSockets with on-the-fly compression, TLS, and a built-in publish-subscribe API — all powered by [uWebSockets](https://github.com/uNetworking/uWebSockets).

<Note>
  Bun's WebSocket server handles \~700,000 messages/second with 16 clients on Linux x64, compared to \~100,000 messages/second with Node.js + `ws`.
</Note>

***

## Basic WebSocket server

Pass a `websocket` object to `Bun.serve()`. Upgrade HTTP connections to WebSocket in the `fetch` handler by calling `server.upgrade(req)`.

```typescript theme={null}
Bun.serve({
  fetch(req, server) {
    if (server.upgrade(req)) return; // upgrade succeeded — no Response needed
    return new Response("Not a WebSocket request", { status: 426 });
  },
  websocket: {
    open(ws) {
      ws.send("Connected!");
    },
    message(ws, msg) {
      ws.send(msg); // echo back
    },
    close(ws) {
      console.log("Connection closed");
    },
  },
});
```

The four lifecycle handlers are:

| Handler                   | Called when                                     |
| ------------------------- | ----------------------------------------------- |
| `open(ws)`                | Client connects                                 |
| `message(ws, message)`    | Client sends a message                          |
| `close(ws, code, reason)` | Connection is closed                            |
| `drain(ws)`               | Backpressure clears and socket is ready to send |

<Note>
  Handlers are declared once per server and shared across all connections. This keeps memory usage flat regardless of how many clients are connected.
</Note>

***

## Sending messages

`ws.send()` accepts strings, `ArrayBuffer`, and `TypedArray`/`DataView` values. It returns a status number:

* `1+` — bytes sent
* `-1` — message enqueued (backpressure)
* `0` — message dropped (connection issue)

```typescript theme={null}
websocket: {
  message(ws, message) {
    ws.send("Hello world");             // string
    ws.send(new Uint8Array([1, 2, 3])); // binary
    ws.send(message, true);             // with per-message compression
  },
},
```

***

## Contextual data

Attach arbitrary data to a connection during upgrade. The data is available on `ws.data` inside all lifecycle handlers. Add a typed `data` property to the `websocket` object for full TypeScript inference.

```typescript theme={null}
type WebSocketData = {
  userId: string;
  channelId: string;
};

Bun.serve({
  fetch(req, server) {
    const url = new URL(req.url);
    server.upgrade(req, {
      data: {
        userId: url.searchParams.get("userId") ?? "anonymous",
        channelId: url.searchParams.get("channel") ?? "general",
      },
    });
    return undefined;
  },
  websocket: {
    // Declare the shape of ws.data for TypeScript
    data: {} as WebSocketData,

    open(ws) {
      console.log(`${ws.data.userId} joined #${ws.data.channelId}`);
    },
    message(ws, message) {
      console.log(`[${ws.data.channelId}] ${ws.data.userId}: ${message}`);
    },
  },
});
```

***

## Custom upgrade headers

Attach headers to the `101 Switching Protocols` response by passing a `headers` object to `server.upgrade()`:

```typescript theme={null}
Bun.serve({
  async fetch(req, server) {
    const sessionId = crypto.randomUUID();
    server.upgrade(req, {
      headers: {
        "Set-Cookie": `SessionId=${sessionId}; HttpOnly; Secure`,
      },
    });
    return undefined;
  },
  websocket: {
    message(ws, msg) { ws.send(msg); },
  },
});
```

***

## Pub/Sub

Bun provides a native topic-based publish-subscribe API. Sockets subscribe to named topics and publish messages that are broadcast to all other subscribers of that topic.

```typescript theme={null}
const server = Bun.serve({
  fetch(req, server) {
    const url = new URL(req.url);
    if (url.pathname === "/chat") {
      const username = url.searchParams.get("name") ?? "anonymous";
      const success = server.upgrade(req, { data: { username } });
      return success
        ? undefined
        : new Response("WebSocket upgrade failed", { status: 400 });
    }
    return new Response("Hello");
  },
  websocket: {
    data: {} as { username: string },

    open(ws) {
      ws.subscribe("the-group-chat");
      server.publish("the-group-chat", `${ws.data.username} joined`);
    },
    message(ws, message) {
      // Broadcast to all other subscribers (excluding sender)
      server.publish("the-group-chat", `${ws.data.username}: ${message}`);
    },
    close(ws) {
      ws.unsubscribe("the-group-chat");
      server.publish("the-group-chat", `${ws.data.username} left`);
    },
  },
});

console.log(`Chat server running at ${server.url}`);
```

<Tip>
  Call `server.publish(topic, message)` (on the `Server` object) to send to **all** subscribers including the calling socket. Call `ws.publish(topic, message)` to send to all subscribers **except** the calling socket.
</Tip>

### Counting subscribers

```typescript theme={null}
Bun.serve({
  fetch(req, server) {
    const count = server.subscriberCount("the-group-chat");
    return new Response(`${count} users online`);
  },
  websocket: {
    message(ws) { ws.subscribe("the-group-chat"); },
  },
});
```

***

## Compression

Enable [per-message deflate](https://datatracker.ietf.org/doc/html/rfc7692) compression globally on the `websocket` handler:

```typescript theme={null}
Bun.serve({
  websocket: {
    perMessageDeflate: true,
    message(ws, msg) { ws.send(msg); },
  },
  fetch(req, server) {
    server.upgrade(req);
    return undefined;
  },
});
```

Compress individual messages by passing `true` as the second argument to `ws.send()`:

```typescript theme={null}
ws.send("This message will be compressed", true);
```

For fine-grained control, configure `compress` and `decompress` separately:

```typescript theme={null}
Bun.serve({
  websocket: {
    perMessageDeflate: {
      compress: "16KB",
      decompress: "shared",
    },
    message(ws, msg) { ws.send(msg); },
  },
  fetch(req, server) {
    server.upgrade(req);
    return undefined;
  },
});
```

Valid compressor values: `"disable"`, `"shared"`, `"dedicated"`, `"3KB"` through `"256KB"`.

***

## Timeouts and limits

```typescript theme={null}
Bun.serve({
  websocket: {
    // Close idle connections after 60 s (default: 120 s)
    idleTimeout: 60,

    // Reject messages larger than 1 MB (default: 16 MB)
    maxPayloadLength: 1024 * 1024,

    // Close the connection when backpressure limit is exceeded
    backpressureLimit: 512 * 1024,
    closeOnBackpressureLimit: true,

    message(ws, msg) { ws.send(msg); },
  },
  fetch(req, server) {
    server.upgrade(req);
    return undefined;
  },
});
```

***

## WebSocket client

Bun implements the standard browser `WebSocket` API. Connect to any `ws://` or `wss://` server:

```typescript theme={null}
const socket = new WebSocket("ws://localhost:3000");

socket.addEventListener("open", () => {
  socket.send("Hello from client!");
});

socket.addEventListener("message", event => {
  console.log("Received:", event.data);
});

socket.addEventListener("close", event => {
  console.log("Disconnected:", event.code, event.reason);
});

socket.addEventListener("error", event => {
  console.error("WebSocket error:", event);
});
```

### Custom headers (Bun extension)

Bun allows setting custom HTTP headers directly in the `WebSocket` constructor. This is a Bun-specific extension and does not work in browsers.

```typescript theme={null}
const socket = new WebSocket("wss://api.example.com/ws", {
  headers: {
    Authorization: "Bearer my-token",
    "X-Client-Version": "2.0",
  },
});
```

### Subprotocol negotiation

```typescript theme={null}
const socket = new WebSocket("ws://localhost:3000", ["soap", "wamp"]);
```

***

## Reference

```typescript theme={null}
interface WebSocketHandler<T = undefined> {
  open?(ws: ServerWebSocket<T>): void | Promise<void>;
  message(ws: ServerWebSocket<T>, message: string | Buffer): void | Promise<void>;
  close?(ws: ServerWebSocket<T>, code: number, reason: string): void | Promise<void>;
  drain?(ws: ServerWebSocket<T>): void | Promise<void>;

  idleTimeout?: number;          // seconds; default: 120
  maxPayloadLength?: number;     // bytes; default: 16 MB
  backpressureLimit?: number;    // bytes; default: 1 MB
  closeOnBackpressureLimit?: boolean; // default: false
  sendPings?: boolean;           // default: true
  publishToSelf?: boolean;       // default: false

  perMessageDeflate?:
    | boolean
    | {
        compress?: boolean | WebSocketCompressor;
        decompress?: boolean | WebSocketCompressor;
      };
}

interface ServerWebSocket<T = undefined> {
  readonly data: T;
  readonly readyState: number;
  readonly remoteAddress: string;
  readonly subscriptions: string[];

  send(message: string | ArrayBuffer | Uint8Array, compress?: boolean): number;
  close(code?: number, reason?: string): void;
  subscribe(topic: string): void;
  unsubscribe(topic: string): void;
  publish(topic: string, message: string | ArrayBuffer | Uint8Array, compress?: boolean): void;
  isSubscribed(topic: string): boolean;
  cork(cb: (ws: ServerWebSocket<T>) => void): void;
}
```
