Bun.serve() supports server-side WebSockets, with on-the-fly compression, TLS support, and a Bun-native publish-subscribe API.

⚡️ 7x more throughput — Bun's WebSockets are fast. For a simple chatroom on Linux x64, Bun can handle 7x more requests per second than Node.js + "ws".

Messages sent per secondRuntimeClients
~700,000(Bun.serve) Bun v0.2.1 (x64)16
~100,000(ws) Node v18.10.0 (x64)16

Internally Bun's WebSocket implementation is built on uWebSockets.

Create a client

To connect to an external socket server, create an instance of WebSocket with the constructor.

const socket = new WebSocket("ws://localhost:3000");

Bun supports setting custom headers. This is a Bun-specific extension of the WebSocket standard.

const socket = new WebSocket("ws://localhost:3000", {
  headers: {
    // custom headers

To add event listeners to the socket:

// message is received
socket.addEventListener("message", event => {});

// socket opened
socket.addEventListener("open", event => {});

// socket closed
socket.addEventListener("close", event => {});

// error handler
socket.addEventListener("error", event => {});

Create a server

Below is a simple WebSocket server built with Bun.serve, in which all incoming requests are upgraded to WebSocket connections in the fetch handler. The socket handlers are declared in the websocket parameter.

  fetch(req, server) {
    // upgrade the request to a WebSocket
    if (server.upgrade(req)) {
      return; // do not return a Response
    return new Response("Upgrade failed :(", { status: 500 });
  websocket: {}, // handlers

The following WebSocket event handlers are supported:

  fetch(req, server) {}, // upgrade logic
  websocket: {
    message(ws, message) {}, // a message is received
    open(ws) {}, // a socket is opened
    close(ws, code, message) {}, // a socket is closed
    drain(ws) {}, // the socket is ready to receive more data

An API designed for speed

The first argument to each handler is the instance of ServerWebSocket handling the event. The ServerWebSocket class is a fast, Bun-native implementation of WebSocket with some additional features.

  fetch(req, server) {}, // upgrade logic
  websocket: {
    message(ws, message) {
      ws.send(message); // echo back the message

Sending messages

Each ServerWebSocket instance has a .send() method for sending messages to the client. It supports a range of input types.

ws.send("Hello world"); // string
ws.send(response.arrayBuffer()); // ArrayBuffer
ws.send(new Uint8Array([1, 2, 3])); // TypedArray | DataView


Once the upgrade succeeds, Bun will send a 101 Switching Protocols response per the spec. Additional headers can be attched to this Response in the call to server.upgrade().

  fetch(req, server) {
    const sessionId = await generateSessionId();
    server.upgrade(req, {
      headers: {
        "Set-Cookie": `SessionId=${sessionId}`,
  websocket: {}, // handlers

Contextual data

Contextual data can be attached to a new WebSocket in the .upgrade() call. This data is made available on the ws.data property inside the WebSocket handlers.

type WebSocketData = {
  createdAt: number;
  channelId: string;

// TypeScript: specify the type of `data`
  fetch(req, server) {
    server.upgrade(req, {
      // TS: this object must conform to WebSocketData
      data: {
        createdAt: Date.now(),
        channelId: new URL(req.url).searchParams.get("channelId"),

    return undefined;
  websocket: {
    // handler called when a message is received
    async message(ws, message) {
      ws.data; // WebSocketData
      await saveMessageToDatabase({
        channel: ws.data.channelId,
        message: String(message),


Bun's ServerWebSocket implementation implements a native publish-subscribe API for topic-based broadcasting. Individual sockets can .subscribe() to a topic (specified with a string identifier) and .publish() messages to all other subscribers to that topic. This topic-based broadcast API is similar to MQTT and Redis Pub/Sub.

const pubsubserver = Bun.serve<{username: string}>({
  fetch(req, server) {
    if (req.url === '/chat') {
      const cookies = getCookieFromRequest(req);
      const success = server.upgrade(req, {
        data: {username: cookies.username},
      return success
        ? undefined
        : new Response('WebSocket upgrade error', {status: 400});

    return new Response('Hello world');
  websocket: {
    open(ws) {
      ws.publish('the-group-chat', `${ws.data.username} has entered the chat`);
    message(ws, message) {
      // this is a group chat
      // so the server re-broadcasts incoming message to everyone
      ws.publish('the-group-chat', `${ws.data.username}: ${message}`);
    close(ws) {
      ws.publish('the-group-chat', `${ws.data.username} has left the chat`);


Per-message compression can be enabled with the perMessageDeflate parameter.

  fetch(req, server) {}, // upgrade logic
  websocket: {
    // enable compression and decompression
    perMessageDeflate: true,

Compression can be enabled for individual messages by passing a boolean as the second argument to .send().

ws.send("Hello world", true);

For fine-grained control over compression characteristics, refer to the Reference.


The .send(message) method of ServerWebSocket returns a number indicating the result of the operation.

  • -1 — The message was enqueued but there is backpressure
  • 0 — The message was dropped due to a connection issue
  • 1+ — The number of bytes sent

This gives you better control over backpressure in your server.


namespace Bun {
  export function serve(params: {
    fetch: (req: Request, server: Server) => Response | Promise<Response>;
    websocket?: {
      message: (ws: ServerWebSocket, message: string | ArrayBuffer | Uint8Array) => void;
      open?: (ws: ServerWebSocket) => void;
      close?: (ws: ServerWebSocket) => void;
      error?: (ws: ServerWebSocket, error: Error) => void;
      drain?: (ws: ServerWebSocket) => void;
        | boolean
        | {
            compress?: boolean | Compressor;
            decompress?: boolean | Compressor;
  }): Server;

type Compressor =
  | `"disable"`
  | `"shared"`
  | `"dedicated"`
  | `"3KB"`
  | `"4KB"`
  | `"8KB"`
  | `"16KB"`
  | `"32KB"`
  | `"64KB"`
  | `"128KB"`
  | `"256KB"`;

interface Server {
  pendingWebsockets: number;
  publish(topic: string, data: string | ArrayBufferView | ArrayBuffer, compress?: boolean): number;
    req: Request,
    options?: {
      headers?: HeadersInit;
      data?: any;
  ): boolean;

interface ServerWebSocket {
  readonly data: any;
  readonly readyState: number;
  readonly remoteAddress: 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): void;
  isSubscribed(topic: string): boolean;
  cork(cb: (ws: ServerWebSocket) => void): void;