Skip to content

Bun WebSocket

Bun has a built-in high-performance WebSocket server that supports real-time bidirectional communication. This chapter introduces how to use Bun WebSocket.

WebSocket Server

Basic Server

typescript
const server = Bun.serve({
  port: 3000,
  
  fetch(request, server) {
    // Upgrade to WebSocket connection
    if (server.upgrade(request)) {
      return; // Upgrade successful
    }
    
    return new Response("Please use WebSocket connection");
  },
  
  websocket: {
    // Connection opened
    open(ws) {
      console.log("Client connected");
      ws.send("Welcome!");
    },
    
    // Message received
    message(ws, message) {
      console.log("Received message:", message);
      ws.send(`You said: ${message}`);
    },
    
    // Connection closed
    close(ws, code, reason) {
      console.log("Connection closed:", code, reason);
    },
    
    // Error occurred
    error(ws, error) {
      console.error("WebSocket error:", error);
    },
  },
});

console.log(`WebSocket server running at ws://localhost:${server.port}`);

Client Connection

javascript
// Browser-side JavaScript
const ws = new WebSocket("ws://localhost:3000");

ws.onopen = () => {
  console.log("Connected");
  ws.send("Hello, Server!");
};

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

ws.onclose = () => {
  console.log("Connection closed");
};

ws.onerror = (error) => {
  console.error("WebSocket error:", error);
};

WebSocket Configuration

Complete Configuration

typescript
Bun.serve({
  port: 3000,
  
  fetch(request, server) {
    // Can pass data during upgrade
    const userId = new URL(request.url).searchParams.get("userId");
    
    const success = server.upgrade(request, {
      // Data passed to websocket handler
      data: {
        userId,
        connectedAt: Date.now(),
      },
    });
    
    if (success) return;
    return new Response("Upgrade failed", { status: 400 });
  },
  
  websocket: {
    // Max message size (bytes)
    maxPayloadLength: 16 * 1024 * 1024, // 16 MB
    
    // Idle timeout (seconds)
    idleTimeout: 120,
    
    // Backpressure limit
    backpressureLimit: 1024 * 1024, // 1 MB
    
    // Whether to auto-close idle connections
    closeOnBackpressureLimit: false,
    
    // Enable compression
    perMessageDeflate: true,
    
    open(ws) {
      // Access passed data
      console.log("User connected:", ws.data.userId);
    },
    
    message(ws, message) {
      console.log(`User ${ws.data.userId} says:`, message);
    },
    
    close(ws) {
      console.log("User disconnected:", ws.data.userId);
    },
  },
});

Message Handling

Sending Messages

typescript
websocket: {
  message(ws, message) {
    // Send text
    ws.send("Text message");
    
    // Send JSON
    ws.send(JSON.stringify({ type: "message", data: "Hello" }));
    
    // Send binary data
    ws.send(new Uint8Array([1, 2, 3, 4]));
    
    // Check send result
    const bytesSent = ws.send("Message");
    console.log("Bytes sent:", bytesSent);
  },
}

Receiving Messages

typescript
websocket: {
  message(ws, message) {
    // message can be string or Buffer
    if (typeof message === "string") {
      console.log("Text message:", message);
      
      // Try to parse JSON
      try {
        const data = JSON.parse(message);
        handleJsonMessage(ws, data);
      } catch {
        handleTextMessage(ws, message);
      }
    } else {
      console.log("Binary message:", message.length, "bytes");
      handleBinaryMessage(ws, message);
    }
  },
}

function handleJsonMessage(ws: ServerWebSocket, data: any) {
  switch (data.type) {
    case "ping":
      ws.send(JSON.stringify({ type: "pong" }));
      break;
    case "message":
      console.log("Received message:", data.content);
      break;
  }
}

Broadcasting and Channels

Pub/Sub Pattern

typescript
const server = Bun.serve({
  port: 3000,
  
  fetch(request, server) {
    const url = new URL(request.url);
    const room = url.searchParams.get("room") || "default";
    
    server.upgrade(request, {
      data: { room },
    });
  },
  
  websocket: {
    open(ws) {
      // Subscribe to channel
      ws.subscribe(ws.data.room);
      console.log(`User joined room: ${ws.data.room}`);
      
      // Broadcast to room
      server.publish(ws.data.room, `New user joined ${ws.data.room}`);
    },
    
    message(ws, message) {
      // Send message to all room members
      server.publish(ws.data.room, message);
    },
    
    close(ws) {
      // Unsubscribe (automatically handled)
      server.publish(ws.data.room, "User left the room");
    },
  },
});

Subscription Management

typescript
websocket: {
  open(ws) {
    // Subscribe to multiple channels
    ws.subscribe("global");
    ws.subscribe(`user:${ws.data.userId}`);
    ws.subscribe("notifications");
  },
  
  message(ws, message) {
    const data = JSON.parse(message as string);
    
    switch (data.action) {
      case "subscribe":
        ws.subscribe(data.channel);
        break;
      case "unsubscribe":
        ws.unsubscribe(data.channel);
        break;
      case "publish":
        server.publish(data.channel, data.message);
        break;
    }
  },
  
  close(ws) {
    // All subscriptions will be automatically cleaned up
  },
}

Connection Management

Tracking Connections

typescript
// Use Set to track all connections
const connections = new Set<ServerWebSocket>();

Bun.serve({
  port: 3000,
  
  fetch(request, server) {
    server.upgrade(request);
  },
  
  websocket: {
    open(ws) {
      connections.add(ws);
      console.log(`Connections: ${connections.size}`);
    },
    
    close(ws) {
      connections.delete(ws);
      console.log(`Connections: ${connections.size}`);
    },
    
    message(ws, message) {
      // Broadcast to all connections
      for (const client of connections) {
        if (client !== ws) {
          client.send(message);
        }
      }
    },
  },
});

// Send heartbeat periodically
setInterval(() => {
  for (const ws of connections) {
    ws.ping();
  }
}, 30000);

Connection Authentication

typescript
Bun.serve({
  port: 3000,
  
  async fetch(request, server) {
    // Validate token
    const url = new URL(request.url);
    const token = url.searchParams.get("token");
    
    if (!token) {
      return new Response("Missing auth token", { status: 401 });
    }
    
    const user = await verifyToken(token);
    if (!user) {
      return new Response("Invalid auth token", { status: 401 });
    }
    
    // Upgrade connection after validation
    server.upgrade(request, {
      data: { user },
    });
  },
  
  websocket: {
    open(ws) {
      console.log(`User ${ws.data.user.name} connected`);
    },
    
    message(ws, message) {
      // Can access user information
      console.log(`${ws.data.user.name}: ${message}`);
    },
  },
});

async function verifyToken(token: string) {
  // Implement token validation logic
  if (token === "valid-token") {
    return { id: 1, name: "Zhang San" };
  }
  return null;
}

Heartbeat and Keep-alive

Server-side Heartbeat

typescript
websocket: {
  open(ws) {
    // Send ping
    ws.ping();
  },
  
  pong(ws) {
    console.log("Received pong");
  },
  
  message(ws, message) {
    // Can also send ping when receiving message
    ws.ping();
  },
}

Client-side Heartbeat

javascript
// Client heartbeat implementation
class WebSocketClient {
  constructor(url) {
    this.url = url;
    this.heartbeatInterval = 30000;
    this.connect();
  }
  
  connect() {
    this.ws = new WebSocket(this.url);
    
    this.ws.onopen = () => {
      console.log("Connected");
      this.startHeartbeat();
    };
    
    this.ws.onclose = () => {
      console.log("Connection closed, attempting to reconnect...");
      this.stopHeartbeat();
      setTimeout(() => this.connect(), 3000);
    };
    
    this.ws.onmessage = (event) => {
      if (event.data === "pong") {
        console.log("Heartbeat OK");
        return;
      }
      // Handle other messages
    };
  }
  
  startHeartbeat() {
    this.heartbeat = setInterval(() => {
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.send("ping");
      }
    }, this.heartbeatInterval);
  }
  
  stopHeartbeat() {
    if (this.heartbeat) {
      clearInterval(this.heartbeat);
    }
  }
}

Chat Room Example

typescript
// chat-server.ts
interface User {
  id: string;
  name: string;
}

interface Message {
  type: "message" | "join" | "leave" | "users";
  user?: string;
  content?: string;
  users?: string[];
  timestamp: number;
}

const users = new Map<ServerWebSocket, User>();

const server = Bun.serve({
  port: 3000,
  
  fetch(request, server) {
    const url = new URL(request.url);
    const name = url.searchParams.get("name");
    
    if (!name) {
      return new Response("Please provide a name: ?name=xxx");
    }
    
    server.upgrade(request, {
      data: {
        id: crypto.randomUUID(),
        name,
      },
    });
  },
  
  websocket: {
    open(ws) {
      const user: User = ws.data;
      users.set(ws, user);
      
      // Subscribe to chat room
      ws.subscribe("chat");
      
      // Broadcast user join
      broadcast({
        type: "join",
        user: user.name,
        timestamp: Date.now(),
      });
      
      // Send current user list
      ws.send(JSON.stringify({
        type: "users",
        users: Array.from(users.values()).map(u => u.name),
        timestamp: Date.now(),
      }));
    },
    
    message(ws, message) {
      const user = users.get(ws);
      if (!user) return;
      
      const data = JSON.parse(message as string);
      
      if (data.type === "message") {
        broadcast({
          type: "message",
          user: user.name,
          content: data.content,
          timestamp: Date.now(),
        });
      }
    },
    
    close(ws) {
      const user = users.get(ws);
      if (user) {
        users.delete(ws);
        
        broadcast({
          type: "leave",
          user: user.name,
          timestamp: Date.now(),
        });
      }
    },
  },
});

function broadcast(message: Message) {
  server.publish("chat", JSON.stringify(message));
}

console.log(`Chat server running at ws://localhost:${server.port}`);

Chat Client

html
<!DOCTYPE html>
<html>
<head>
  <title>Bun Chat Room</title>
  <style>
    #messages { height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; }
    .message { margin: 5px 0; }
    .join { color: green; }
    .leave { color: red; }
  </style>
</head>
<body>
  <div id="messages"></div>
  <input type="text" id="input" placeholder="Enter message...">
  <button onclick="sendMessage()">Send</button>
  
  <script>
    const name = prompt("Please enter your name:");
    const ws = new WebSocket(`ws://localhost:3000?name=${encodeURIComponent(name)}`);
    const messages = document.getElementById("messages");
    
    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      const div = document.createElement("div");
      div.className = "message";
      
      switch (data.type) {
        case "message":
          div.textContent = `${data.user}: ${data.content}`;
          break;
        case "join":
          div.className += " join";
          div.textContent = `${data.user} joined the chat room`;
          break;
        case "leave":
          div.className += " leave";
          div.textContent = `${data.user} left the chat room`;
          break;
        case "users":
          div.textContent = `Online users: ${data.users.join(", ")}`;
          break;
      }
      
      messages.appendChild(div);
      messages.scrollTop = messages.scrollHeight;
    };
    
    function sendMessage() {
      const input = document.getElementById("input");
      if (input.value) {
        ws.send(JSON.stringify({ type: "message", content: input.value }));
        input.value = "";
      }
    }
    
    document.getElementById("input").addEventListener("keypress", (e) => {
      if (e.key === "Enter") sendMessage();
    });
  </script>
</body>
</html>

WebSocket Client

Bun as Client

typescript
// Bun can also act as a WebSocket client
const ws = new WebSocket("ws://localhost:3000");

ws.addEventListener("open", () => {
  console.log("Connected to server");
  ws.send("Hello, Server!");
});

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

ws.addEventListener("close", () => {
  console.log("Connection closed");
});

// Keep process running
await Bun.sleep(Infinity);

Summary

This chapter introduced:

  • ✅ Creating WebSocket servers
  • ✅ Sending and receiving messages
  • ✅ Pub/Sub broadcast pattern
  • ✅ Connection management and authentication
  • ✅ Heartbeat and keep-alive mechanisms
  • ✅ Complete chat room example

Next Steps

Continue reading Fetch API to learn about Bun's network request features.

Content is for learning and research only.