home / skills / secondsky / claude-skills / bun-websocket-server

bun-websocket-server skill

/plugins/bun/skills/bun-websocket-server

This skill helps you implement WebSocket servers in Bun using Bun.serve, enabling real-time messaging, pub/sub, and client broadcasting.

npx playbooks add skill secondsky/claude-skills --skill bun-websocket-server

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
7.0 KB
---
name: Bun WebSocket Server
description: This skill should be used when the user asks about "WebSocket in Bun", "real-time communication", "Bun.serve websocket", "ws server", "socket connections", "pub/sub", "broadcasting messages", "WebSocket upgrade", or building real-time applications with Bun.
version: 1.0.0
---

# Bun WebSocket Server

Bun has built-in WebSocket support integrated with `Bun.serve()`.

## Quick Start

```typescript
const server = Bun.serve({
  fetch(req, server) {
    // Upgrade to WebSocket
    if (server.upgrade(req)) {
      return; // Upgraded successfully
    }
    return new Response("Not a WebSocket request", { status: 400 });
  },
  websocket: {
    open(ws) {
      console.log("Client connected");
    },
    message(ws, message) {
      console.log("Received:", message);
      ws.send(`Echo: ${message}`);
    },
    close(ws) {
      console.log("Client disconnected");
    },
  },
});

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

## WebSocket Handlers

```typescript
Bun.serve({
  fetch(req, server) {
    server.upgrade(req);
  },
  websocket: {
    // Client connected
    open(ws) {
      console.log("New connection");
    },

    // Message received
    message(ws, message) {
      // message is string | Buffer
      if (typeof message === "string") {
        console.log("Text:", message);
      } else {
        console.log("Binary:", message);
      }
    },

    // Connection closed
    close(ws, code, reason) {
      console.log(`Closed: ${code} - ${reason}`);
    },

    // Drain event (buffer flushed)
    drain(ws) {
      console.log("Buffer drained");
    },

    // Ping received
    ping(ws, data) {
      // Pong sent automatically
    },

    // Pong received
    pong(ws, data) {
      console.log("Pong received");
    },
  },
});
```

## Sending Messages

```typescript
websocket: {
  message(ws, message) {
    // Send text
    ws.send("Hello");

    // Send JSON
    ws.send(JSON.stringify({ type: "greeting", data: "Hello" }));

    // Send binary
    ws.send(new Uint8Array([1, 2, 3]));
    ws.send(Buffer.from("binary data"));

    // Send with compression
    ws.send("compressed message", true);

    // Check if buffer is full
    const bufferedAmount = ws.send("data");
    if (bufferedAmount > 1024 * 1024) {
      console.log("Buffer getting full");
    }
  },
}
```

## Attaching Data to Connections

```typescript
interface UserData {
  id: string;
  name: string;
  joinedAt: Date;
}

Bun.serve<UserData>({
  fetch(req, server) {
    const url = new URL(req.url);
    const userId = url.searchParams.get("userId");

    // Attach data during upgrade
    server.upgrade(req, {
      data: {
        id: userId,
        name: "User " + userId,
        joinedAt: new Date(),
      },
    });
  },
  websocket: {
    open(ws) {
      // Access attached data
      console.log(`${ws.data.name} connected`);
    },
    message(ws, message) {
      console.log(`${ws.data.name}: ${message}`);
    },
  },
});
```

## Pub/Sub (Topics)

```typescript
Bun.serve({
  fetch(req, server) {
    const url = new URL(req.url);
    const room = url.searchParams.get("room") || "general";

    server.upgrade(req, {
      data: { room },
    });
  },
  websocket: {
    open(ws) {
      // Subscribe to a topic
      ws.subscribe(ws.data.room);

      // Publish to topic (excludes sender)
      ws.publish(ws.data.room, `User joined ${ws.data.room}`);
    },
    message(ws, message) {
      // Broadcast to all in room (excludes sender)
      ws.publish(ws.data.room, message);
    },
    close(ws) {
      // Unsubscribe (automatic on close)
      ws.unsubscribe(ws.data.room);
      ws.publish(ws.data.room, "User left");
    },
  },
});
```

## Broadcasting to All Clients

```typescript
Bun.serve({
  fetch(req, server) {
    server.upgrade(req);
  },
  websocket: {
    open(ws) {
      // Subscribe to global topic
      ws.subscribe("global");
    },
    message(ws, message) {
      // Broadcast to ALL clients including sender
      server.publish("global", message);
    },
  },
});
```

## Server-Level Publish

```typescript
const server = Bun.serve({
  fetch(req, server) {
    const url = new URL(req.url);

    // HTTP endpoint to publish
    if (url.pathname === "/broadcast") {
      const message = url.searchParams.get("msg");
      server.publish("global", message);
      return new Response("Broadcasted");
    }

    server.upgrade(req);
  },
  websocket: {
    open(ws) {
      ws.subscribe("global");
    },
  },
});

// Can also publish from outside fetch
setInterval(() => {
  server.publish("global", `Server time: ${new Date().toISOString()}`);
}, 5000);
```

## WebSocket Options

```typescript
Bun.serve({
  websocket: {
    // Max message size (default 16MB)
    maxPayloadLength: 1024 * 1024, // 1MB

    // Idle timeout in seconds (default 120)
    idleTimeout: 60,

    // Backpressure limit
    backpressureLimit: 1024 * 1024,

    // Enable compression
    perMessageDeflate: true,
    // Or with options
    perMessageDeflate: {
      compress: "shared",
      decompress: "shared",
    },

    // Send/receive pings
    sendPings: true,

    // Handlers
    open(ws) {},
    message(ws, message) {},
    close(ws) {},
  },
});
```

## Client-Side Connection

```javascript
// Browser
const ws = new WebSocket("ws://localhost:3000");

ws.onopen = () => {
  ws.send("Hello Server!");
};

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

ws.onclose = () => {
  console.log("Disconnected");
};
```

## Authentication

```typescript
Bun.serve({
  fetch(req, server) {
    // Verify auth before upgrade
    const token = req.headers.get("Authorization");

    if (!verifyToken(token)) {
      return new Response("Unauthorized", { status: 401 });
    }

    const user = decodeToken(token);
    server.upgrade(req, {
      data: { userId: user.id },
    });
  },
  websocket: {
    open(ws) {
      console.log(`Authenticated user ${ws.data.userId} connected`);
    },
  },
});
```

## Common Errors

| Error | Cause | Fix |
|-------|-------|-----|
| `Upgrade failed` | Invalid request | Check upgrade headers |
| `Connection closed` | Client disconnect | Handle in close handler |
| `Message too large` | Exceeds maxPayloadLength | Increase limit or chunk data |
| `Backpressure` | Slow client | Check buffer, wait for drain |

## Common Patterns

### Chat Room

```typescript
Bun.serve({
  fetch(req, server) {
    const url = new URL(req.url);
    const username = url.searchParams.get("user") || "Anonymous";

    server.upgrade(req, {
      data: { username },
    });
  },
  websocket: {
    open(ws) {
      ws.subscribe("chat");
      ws.publish("chat", `${ws.data.username} joined`);
    },
    message(ws, message) {
      ws.publish("chat", `${ws.data.username}: ${message}`);
    },
    close(ws) {
      ws.publish("chat", `${ws.data.username} left`);
    },
  },
});
```

## When to Load References

Load `references/compression.md` when:
- perMessageDeflate configuration
- Compression tuning
- Binary message handling

Load `references/scaling.md` when:
- Multiple server instances
- Redis pub/sub integration
- Horizontal scaling

Overview

This skill explains how to build WebSocket servers using Bun. It covers upgrading HTTP requests, handling lifecycle events, attaching per-connection data, pub/sub topics, broadcasting, and server-side publishing patterns. It is focused on production-ready patterns, options, and common pitfalls for real-time apps with Bun.

How this skill works

Bun provides built-in WebSocket support via Bun.serve(), where fetch can call server.upgrade(req) to turn an HTTP request into a WebSocket connection. The websocket handlers (open, message, close, ping, pong, drain) let you manage connection lifecycle and send text, JSON, or binary frames. Server and connection APIs support subscribe/publish for topics, attaching typed data to each connection, and configurable options like maxPayloadLength, idleTimeout, backpressure limits, and compression.

When to use it

  • Building chat, presence, or collaborative apps requiring low-latency bidirectional communication
  • Implementing pub/sub or room/topic-based messaging within a single Bun server
  • Broadcasting server-originated updates (timestamps, notifications) to many clients
  • Handling binary protocols or streaming small binary payloads over WebSocket
  • Adding simple auth or metadata during upgrade to identify connections

Best practices

  • Verify and authenticate clients in fetch before calling server.upgrade to prevent unauthorized connections
  • Attach typed data to connections (e.g., user id, room) to simplify handler logic and avoid global state
  • Use ws.subscribe/ws.publish for room-based messaging and server.publish for global broadcasts
  • Tune maxPayloadLength and backpressureLimit to protect memory and handle slow clients; use drain events to resume sending
  • Enable perMessageDeflate carefully and test compression effects on latency and CPU; load compression reference for advanced tuning

Example use cases

  • Chat room with join/leave notifications using ws.subscribe and ws.publish
  • Realtime dashboard broadcasting server metrics with server.publish from a timer
  • Authenticated WebSocket where token is validated in fetch and userId is attached to ws.data
  • Multiplayer game lobby with per-connection state and topic-based matchmaking
  • HTTP endpoint that triggers a broadcast to all connected clients for admin messages

FAQ

How do I attach user info to a connection?

Decode or validate auth in fetch, then call server.upgrade(req, { data: { userId, ... } }). Access it on ws.data in handlers.

How do I broadcast to all clients including the sender?

Subscribe connections to a global topic and call server.publish('global', message) to send to all subscribers, including the sender.