Skip to main content
Bun provides fast, built-in WebSocket support via Bun.serve(). WebSocket connections are upgraded from HTTP requests and support pub/sub messaging, backpressure handling, and compression.

Basic WebSocket Server

Bun.serve({
  websocket: {
    open(ws) {
      console.log("Client connected");
    },
    message(ws, message) {
      console.log("Received:", message);
      ws.send(message); // Echo back
    },
    close(ws, code, reason) {
      console.log("Client disconnected", code, reason);
    },
  },
  fetch(req, server) {
    const url = new URL(req.url);
    if (url.pathname === "/chat") {
      const upgraded = server.upgrade(req);
      if (upgraded) return; // Don't return a Response
    }
    return new Response("Hello HTTP");
  },
});

WebSocket Handler Configuration

Message Handler

websocket: {
  message(ws, message) {
    // message type depends on binaryType
    if (typeof message === "string") {
      console.log("Text message:", message);
    } else {
      console.log("Binary message:", message);
    }
  },
}

Binary Type

websocket: {
  open(ws) {
    // Set how binary data is returned
    ws.binaryType = "nodebuffer"; // Default
    // ws.binaryType = "arraybuffer";
    // ws.binaryType = "uint8array";
  },
  message(ws, message) {
    // Type of message depends on binaryType:
    // "nodebuffer" => Buffer
    // "arraybuffer" => ArrayBuffer  
    // "uint8array" => Uint8Array
  },
}

Upgrading HTTP Connections

Basic Upgrade

fetch(req, server) {
  const success = server.upgrade(req);
  if (success) {
    // WebSocket upgrade successful, don't return a Response
    return;
  }
  return new Response("Upgrade failed", { status: 400 });
}

Upgrade with Custom Data

interface WebSocketData {
  userId: string;
  username: string;
}

Bun.serve<WebSocketData>({
  websocket: {
    message(ws, message) {
      console.log(`${ws.data.username} says: ${message}`);
    },
  },
  fetch(req, server) {
    const url = new URL(req.url);
    const username = url.searchParams.get("name") || "Anonymous";
    
    const upgraded = server.upgrade(req, {
      data: {
        userId: crypto.randomUUID(),
        username,
      },
    });
    
    if (upgraded) return;
    return new Response("Upgrade failed", { status: 400 });
  },
});

Upgrade with Custom Headers

server.upgrade(req, {
  headers: {
    "Set-Cookie": "session=abc123",
  },
});

Sending Messages

Send Text or Binary

// Send text
ws.send("Hello");

// Send binary
ws.send(new Uint8Array([1, 2, 3, 4]));

// With compression
ws.send("Compress this message", true);

Explicit Text/Binary

// Always send as text
const status = ws.sendText("Hello", compress);

// Always send as binary
const status = ws.sendBinary(new Uint8Array([1, 2, 3]), compress);

Send Status

All send methods return a status number:
const status = ws.send("Hello");

if (status === 0) {
  console.log("Message was dropped");
} else if (status === -1) {
  console.log("Backpressure applied");
} else {
  console.log(`Sent ${status} bytes`);
}

Pub/Sub Messaging

Subscribe to Topics

websocket: {
  open(ws) {
    // Subscribe to topics
    ws.subscribe("chat");
    ws.subscribe("notifications");
  },
  message(ws, message) {
    // Publish to all subscribers in a topic
    ws.publish("chat", message);
  },
}

Check Subscriptions

if (ws.isSubscribed("chat")) {
  console.log("Subscribed to chat");
}

// Get all subscriptions
const topics = ws.subscriptions;
console.log("Subscribed to:", topics); // ["chat", "notifications"]

Unsubscribe

ws.unsubscribe("chat");

Publish from HTTP Handler

Bun.serve({
  websocket: {
    open(ws) {
      ws.subscribe("global");
    },
  },
  fetch(req, server) {
    if (req.method === "POST") {
      const data = await req.text();
      // Publish to all WebSocket subscribers
      server.publish("global", data);
      return new Response("Published");
    }
    return new Response("Send POST to publish");
  },
});

Subscriber Count

const count = server.subscriberCount("chat");
console.log(`${count} clients in chat`);

Ping/Pong

websocket: {
  open(ws) {
    // Manual ping
    ws.ping("optional data");
  },
  ping(ws, data) {
    console.log("Received ping", data);
  },
  pong(ws, data) {
    console.log("Received pong", data);
  },
}

Automatic Pings

websocket: {
  sendPings: true, // Default: true
  // Server automatically sends pings
}

Connection Management

Closing Connections

// Graceful close
ws.close(1000, "Normal closure");

// Abrupt termination
ws.terminate();

Close Codes

  • 1000 - Normal closure (default)
  • 1009 - Message too big
  • 1011 - Server error
  • 1012 - Server restarting
  • 1013 - Try again later
  • 4000-4999 - Application-specific codes

Ready State

const state = ws.readyState;
// 0 = CONNECTING
// 1 = OPEN
// 2 = CLOSING
// 3 = CLOSED

Remote Address

console.log(ws.remoteAddress); // "127.0.0.1"

Backpressure and Buffering

Check Buffered Amount

const buffered = ws.getBufferedAmount();
if (buffered > 1024 * 1024) {
  console.log("Too much data buffered");
}

Drain Handler

Called when backpressure is relieved:
websocket: {
  drain(ws) {
    console.log("Ready to send more data");
    // Send queued messages
  },
}

Backpressure Limits

websocket: {
  backpressureLimit: 1024 * 1024 * 16, // 16 MB (default)
  closeOnBackpressureLimit: false, // Don't auto-close
}

Corking

Batch multiple send operations:
ws.cork((ws) => {
  ws.send("Message 1");
  ws.send("Message 2");
  ws.send("Message 3");
  // All sent together
});

Configuration Options

websocket: {
  // Size limits
  maxPayloadLength: 16 * 1024 * 1024, // 16 MB (default)
  backpressureLimit: 16 * 1024 * 1024, // 16 MB (default)
  closeOnBackpressureLimit: false,
  
  // Timeouts
  idleTimeout: 120, // seconds (default)
  
  // Pub/sub
  publishToSelf: false, // Don't echo publishes to sender
  
  // Pings
  sendPings: true, // Auto-send pings
  
  // Compression
  perMessageDeflate: {
    compress: true,
    decompress: true,
  },
}

Compression

websocket: {
  perMessageDeflate: {
    compress: "3KB", // Compress messages > 3KB
    decompress: true,
  },
  // Or use boolean:
  // perMessageDeflate: true,
}
Compression levels: "disable", "shared", "dedicated", "3KB", "4KB", "8KB", "16KB", "32KB", "64KB", "128KB", "256KB"

Type Signatures

interface ServerWebSocket<T = undefined> {
  send(data: string | BufferSource, compress?: boolean): number;
  sendText(data: string, compress?: boolean): number;
  sendBinary(data: BufferSource, compress?: boolean): number;
  
  close(code?: number, reason?: string): void;
  terminate(): void;
  
  ping(data?: string | BufferSource): number;
  pong(data?: string | BufferSource): number;
  
  publish(topic: string, data: string | BufferSource, compress?: boolean): number;
  publishText(topic: string, data: string, compress?: boolean): number;
  publishBinary(topic: string, data: BufferSource, compress?: boolean): number;
  
  subscribe(topic: string): void;
  unsubscribe(topic: string): void;
  isSubscribed(topic: string): boolean;
  
  cork<T>(callback: (ws: ServerWebSocket) => T): T;
  getBufferedAmount(): number;
  
  readonly subscriptions: string[];
  readonly remoteAddress: string;
  readonly readyState: 0 | 1 | 2 | 3;
  binaryType?: "nodebuffer" | "arraybuffer" | "uint8array";
  data: T;
}

Build docs developers (and LLMs) love