# How to Use WebSockets in Bun for Real-Time Bidirectional Communication

> Learn how to use WebSockets in Bun for real-time bidirectional communication. Bun's native implementation makes upgrading HTTP requests and handling messages simple and efficient.

- Repository: [Bun/bun](https://github.com/oven-sh/bun)
- Tags: how-to-guide
- Published: 2026-02-28

---

**Bun provides a native WebSocket implementation built on uWebSockets that lets you upgrade HTTP requests to persistent connections using `Bun.serve()` and handle bidirectional messaging through a shared handler object.**

Bun's WebSocket support is engineered for high-performance real-time applications. According to the oven-sh/bun source code, the runtime exposes uWebSockets through a JavaScript API that minimizes memory overhead while maximizing throughput. This guide explains how to implement bidirectional communication using Bun's native `ServerWebSocket` class and the `Bun.serve()` infrastructure.

## Creating a WebSocket Server with Bun.serve()

The foundation of Bun's WebSocket support lies in the `Bun.serve()` API. When you configure a server, you provide a `fetch` handler to process HTTP requests and a `websocket` object containing event handlers. The upgrade mechanism that transforms HTTP into WebSocket connections is implemented in `src/http/websocket_http_client.zig`, while the core socket logic resides in `src/http/websocket.zig`.

### Upgrading HTTP Requests to WebSockets

To establish a WebSocket connection, call `server.upgrade(req, options?)` inside your `fetch` handler. If the upgrade succeeds, the function returns `true` and you should return `undefined` (or no response) to let Bun handle the protocol switch. If it fails, return a standard HTTP Response.

```typescript
// server.ts
Bun.serve({
  fetch(req, server) {
    // Upgrade any request that reaches this point.
    if (server.upgrade(req)) return;               // success → no Response
    return new Response("Upgrade failed", { status: 500 });
  },
  websocket: {
    // Echo every incoming message back to the sender.
    message(ws, message) {
      ws.send(message);
    },
  },
});

```

*Source reference:* This pattern is documented in `docs/runtime/http/websockets.mdx` and implemented in the Zig upgrade handler at `src/http/websocket_http_client.zig`.

### The Shared Handler Architecture

Unlike other runtimes that instantiate new handler objects per connection, Bun uses a **shared handler object** defined in the `websocket` property of `Bun.serve()`. This design, optimized in `src/http/websocket.zig`, keeps memory overhead low even with thousands of concurrent connections—only one set of callbacks exists, and each `ServerWebSocket` instance holds minimal per-connection state.

## Managing WebSocket Connections and Data

Once upgraded, connections are managed through the `ServerWebSocket` interface. This class provides methods for sending data, managing subscriptions, and inspecting connection health.

### Typed Context Data

You can attach contextual data during the upgrade process using the `data` option. This data becomes available on the `ws` object inside all lifecycle handlers, enabling type-safe access to user information or room identifiers.

```typescript
type WSData = { userId: string; room: string };

Bun.serve({
  fetch(req, server) {
    const token = new Bun.CookieMap(req.headers.get("cookie")!).get("auth");
    server.upgrade(req, {
      data: { userId: token, room: new URL(req.url).searchParams.get("room")! },
    });
    return undefined; // upgrade handled
  },
  websocket: {
    data: {} as WSData, // tells TypeScript the shape of ws.data
    async message(ws, msg) {
      // ws.data is strongly typed here
      await saveMessage(ws.data.userId, ws.data.room, String(msg));
    },
  },
});

```

*Source reference:* The contextual data pattern is documented in `docs/runtime/http/websockets.mdx` (lines 25-31).

### Connection Lifecycle Events

The shared handler object supports five key events defined in `src/http/websocket.zig`:
- **`open(ws)`**: Fired when the connection is established.
- **`message(ws, message)`**: Fired when the client sends data (string or Buffer).
- **`close(ws, code, reason)`**: Fired when the connection closes.
- **`drain(ws)`**: Fired when the socket is ready for more data after back-pressure.
- **`error(ws, error)`**: Fired when a protocol error occurs.

## Advanced WebSocket Features in Bun

Bun's implementation includes production-ready features like pub/sub broadcasting, compression, and back-pressure management, all leveraging uWebSockets' native capabilities.

### Pub/Sub Broadcasting with Topics

Bun supports topic-based messaging via the `subscribe()` and `publish()` methods. This enables efficient broadcasting without iterating through connections manually. The pub/sub logic uses uWebSockets' built-in implementation as exposed through `src/http/websocket.zig`.

```typescript
const server = Bun.serve({
  fetch(req, server) {
    if (new URL(req.url).pathname === "/chat") {
      const username = getUsernameFromReq(req);
      server.upgrade(req, { data: { username } });
      return undefined;
    }
    return new Response("Not a WebSocket endpoint");
  },
  websocket: {
    data: {} as { username: string },
    open(ws) {
      ws.subscribe("room");
      server.publish("room", `${ws.data.username} joined`);
    },
    message(ws, msg) {
      server.publish("room", `${ws.data.username}: ${msg}`);
    },
    close(ws) {
      ws.unsubscribe("room");
      server.publish("room", `${ws.data.username} left`);
    },
  },
});

console.log(`Listening on ${server.hostname}:${server.port}`);

```

### Per-Message Compression

Enable compression via the `perMessageDeflate` option in the server configuration, or control it per-message via the second argument to `send()`. The compression handling is wired through uWebSockets in `src/http/websocket_client.zig`.

```typescript
Bun.serve({
  websocket: {
    perMessageDeflate: true, // enables default compression
    message(ws, msg) {
      ws.send(msg, true); // compress this individual message
    },
  },
});

```

### Back-Pressure and Flow Control

The `send()` method returns a numeric status indicating the operation result: `-1` means the data was enqueued due to back-pressure, `0` means it was dropped, and positive values indicate the number of bytes sent. Monitor the `drain` event to resume sending when the buffer clears.

```typescript
Bun.serve({
  websocket: {
    message(ws, msg) {
      const result = ws.send(msg);
      if (result === -1) {
        // The socket buffer is full – stop sending until 'drain' fires
        console.warn("Back‑pressure, pausing sends");
      }
    },
    drain(ws) {
      console.log("Socket ready for more data");
    },
  },
});

```

*Source reference:* Back-pressure semantics are documented at lines 69-76 of `docs/runtime/http/websockets.mdx` and implemented in `src/http/websocket.zig`.

## Connecting from the Client Side

Bun ships with a `WebSocket` class that matches the browser API but includes a `headers` option for non-browser use cases. The implementation lives in [`src/bake/client/websocket.ts`](https://github.com/oven-sh/bun/blob/main/src/bake/client/websocket.ts) and wraps the native client code from `src/http/websocket_client.zig`.

```typescript
// client.ts (runs with `bun run client.ts`)
const socket = new WebSocket("ws://localhost:3000/chat", {
  headers: { "X-Auth": "my-secret-token" }, // Bun‑only extension
});

socket.addEventListener("open", () => console.log("Connected"));
socket.addEventListener("message", e => console.log("←", e.data));
socket.addEventListener("close", () => console.log("Disconnected"));
socket.addEventListener("error", err => console.error("❌", err));

// Send a message
socket.send("Hello from Bun!");

```

## Summary

- Bun's WebSocket implementation is built on the high-performance uWebSockets C++ library and exposed through `Bun.serve()`.
- Use `server.upgrade(req)` in the fetch handler to promote HTTP connections to WebSockets, as implemented in `src/http/websocket_http_client.zig`.
- The shared handler pattern in `src/http/websocket.zig` keeps memory usage low for thousands of concurrent connections.
- Return values from `ws.send()` indicate back-pressure status (`-1` for queued, `0` for dropped, positive for bytes sent).
- Topic-based pub/sub is supported natively via `ws.subscribe()` and `server.publish()` using uWebSockets' built-in mechanisms.
- Per-message compression is available through the `perMessageDeflate` option or per-send boolean flags.

## Frequently Asked Questions

### How do I upgrade an HTTP request to a WebSocket in Bun?

Call `server.upgrade(req, options?)` inside the `fetch` handler of your `Bun.serve()` configuration. If the upgrade succeeds, return `undefined` to complete the handshake; otherwise return a Response object. The upgrade logic is implemented in `src/http/websocket_http_client.zig` and exposed through the Server interface in `src/http/websocket.zig`.

### What is the difference between Bun's ServerWebSocket and the standard WebSocket API?

`ServerWebSocket` is Bun's server-side class (defined in `src/http/websocket.zig`) that wraps uWebSockets and provides methods like `publish()` and `subscribe()` for pub/sub, plus numeric back-pressure reporting. The standard `WebSocket` class (implemented in [`src/bake/client/websocket.ts`](https://github.com/oven-sh/bun/blob/main/src/bake/client/websocket.ts)) is for clients and matches the browser API with Bun-specific extensions like custom headers.

### How does Bun handle WebSocket back-pressure?

The `send()` method returns a number indicating the send status: `-1` means the data was enqueued due to back-pressure, `0` means it was dropped, and positive values indicate bytes sent. You should monitor the `drain` event to resume sending when the buffer clears, as implemented in the Zig layer of `src/http/websocket.zig`.

### Can I use compression with Bun WebSockets?

Yes. Enable `perMessageDeflate: true` in the websocket configuration for default compression, or pass `true` as the second argument to `ws.send(message, compress)` for per-message control. The compression handling is wired through uWebSockets in `src/http/websocket_client.zig`.