Skip to main content
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.
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.

Basic WebSocket server

Pass a websocket object to Bun.serve(). Upgrade HTTP connections to WebSocket in the fetch handler by calling server.upgrade(req).
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:
HandlerCalled 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
Handlers are declared once per server and shared across all connections. This keeps memory usage flat regardless of how many clients are connected.

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)
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.
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():
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.
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}`);
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.

Counting subscribers

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 compression globally on the websocket handler:
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():
ws.send("This message will be compressed", true);
For fine-grained control, configure compress and decompress separately:
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

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:
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.
const socket = new WebSocket("wss://api.example.com/ws", {
  headers: {
    Authorization: "Bearer my-token",
    "X-Client-Version": "2.0",
  },
});

Subprotocol negotiation

const socket = new WebSocket("ws://localhost:3000", ["soap", "wamp"]);

Reference

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;
}

Build docs developers (and LLMs) love