Skip to main content
skill CX Codex

bun-websocket-server — Claude Code skill for WebSocket in Bun

A skill packed with Bun WebSocket server patterns: handlers, pub/sub, broadcasting. Handy when you need a chat or real-time streaming without extra libraries.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add secondsky/claude-skills --skill bun-websocket-server -a claude-code
Open on GitHub

What bun-websocket-server does

The skill covers the WebSocket support built into Bun.serve(). It ships TypeScript snippets for request upgrade, open, message, close, drain, ping, and pong handlers. There's also a section on sending text, JSON, and binary via ws.send(), with compression and buffer checks.

Pub/sub is the main pattern: clients subscribe with ws.subscribe(), messages go out through ws.publish() or server.publish(). The skill walks through chat rooms, broadcast to all clients, attaching user data during upgrade, and token authentication before the upgrade happens.

Server options include maxPayloadLength, idleTimeout, backpressureLimit, perMessageDeflate, and sendPings. A table covers common errors like Upgrade failed and Message too large. The source is truncated, so treat this as a pattern collection rather than full Bun documentation.

What it helps with

  • Run a WebSocket server on Bun with no third-party packages
  • Build chat rooms with subscribe/publish
  • Broadcast a message to every connected client
  • Attach user data to a WebSocket connection
  • Reject unauthorized upgrades before they happen

Example prompts for the agent

  • Set up a Bun WebSocket server that echoes messages back
  • Add pub/sub rooms: client sends a room name, messages stay in that room
  • Show me how to send binary data with ws.send

How to install bun-websocket-server

1Skills CLI (Vercel)

npx skills add secondsky/claude-skills --skill bun-websocket-server -a claude-code

2Manually: put the files in place

~/.claude/skills/bun-websocket-server/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/bun-websocket-server/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about bun-websocket-server

How do I install bun-websocket-server in Claude Code?

Run npx skills add secondsky/claude-skills --skill bun-websocket-server -a claude-code. The skill lands in ~/.claude/skills/bun-websocket-server/SKILL.md.

What does bun-websocket-server do?

It provides ready-made snippets for a Bun WebSocket server: request upgrade, connection handlers, text and binary sending, topic-based pub/sub, broadcasting, and authentication.

Do I need a separate WebSocket library with Bun?

No. The skill builds on the WebSocket support already inside Bun.serve(), so no extra packages are involved.

Source text

SKILL.md · MIT
---
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.
metadata:
  version: "1.0.0"
license: MIT
---

# 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

© secondsky · MIT · secondsky/claude-skills

Similar skills

Everything for Backend and APIs →

From the same set89

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor