---
title: "Build a Bun + WebSocket Voice Agent with Bun.serve Patterns"
description: "Bun's native Bun.serve WebSocket API outperforms Node ws by 4-7x on idle sockets. Build a full Realtime voice bridge in TypeScript with binary frames and pub/sub."
canonical: https://callsphere.ai/blog/vw2h-build-bun-websocket-voice-agent-bun-serve
category: "AI Engineering"
tags: ["Tutorial", "Build", "Bun", "WebSocket", "TypeScript"]
author: "CallSphere Team"
published: 2026-04-02T00:00:00.000Z
updated: 2026-05-07T09:27:40.418Z
---

# Build a Bun + WebSocket Voice Agent with Bun.serve Patterns

> Bun's native Bun.serve WebSocket API outperforms Node ws by 4-7x on idle sockets. Build a full Realtime voice bridge in TypeScript with binary frames and pub/sub.

> **TL;DR** — Bun's WebSocket isn't a library — it's part of `Bun.serve`. No `ws` install, native pub/sub, and 4-7x throughput vs Node on idle voice sockets. Perfect for a Realtime bridge.

## What you'll build

A single-file Bun server that upgrades browser clients to WebSocket, opens a paired client to OpenAI Realtime via the standard `WebSocket` global, and pumps frames between them. We add per-call session IDs, a basic rate limit, and a /healthz route.

## Prerequisites

1. Bun 1.1+ installed (`curl -fsSL https://bun.sh/install | bash`).
2. `OPENAI_API_KEY` set in env.
3. A static frontend that connects to `ws://localhost:8080/voice`.
4. Familiarity with TypeScript and the WebSocket API.
5. `bun init`-ed project.

## Architecture

```mermaid
flowchart LR
  B[Browser] -- ws --> S[Bun.serve]
  S -- ws --> O[OpenAI Realtime]
  S -- pubsub --> S2[(other Bun nodes)]
```

## Step 1 — Bun.serve with WebSocket upgrade

```typescript
type SessionData = { id: string; oai: WebSocket };

const server = Bun.serve({
  port: 8080,
  fetch(req, srv) {
    const url = new URL(req.url);
    if (url.pathname === "/healthz") return new Response("ok");
    if (url.pathname !== "/voice") return new Response("nf", { status: 404 });

```
const id = crypto.randomUUID();
if (srv.upgrade(req, { data: { id, oai: undefined as any } })) return;
return new Response("Upgrade failed", { status: 400 });
```

},
  websocket: {
    open(ws) { handleOpen(ws); },
    message(ws, msg) { handleMessage(ws, msg); },
    close(ws) { ws.data.oai?.close(); },
  },
});
console.log("listening", server.port);
```

## Step 2 — Open the OpenAI socket on `open`

Bun's global `WebSocket` supports protocols and headers — perfect for Realtime auth.

```typescript
function handleOpen(ws: any) {
  const oai = new WebSocket(
    "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2025-06-03",
    {
      headers: {
        Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
        "OpenAI-Beta": "realtime=v1",
      },
    } as any
  );
  ws.data.oai = oai;

oai.addEventListener("open", () => {
    oai.send(JSON.stringify({
      type: "session.update",
      session: {
        instructions: "You are CallSphere's Bun-powered agent.",
        voice: "alloy",
        input_audio_format: "pcm16",
        output_audio_format: "pcm16",
        turn_detection: { type: "server_vad" }
      }
    }));
    ws.subscribe(`call:${ws.data.id}`);
  });

oai.addEventListener("message", (e) => {
    server.publish(`call:${ws.data.id}`, e.data as string);
  });
}
```

## Step 3 — Forward client → OpenAI

```typescript
function handleMessage(ws: any, raw: string | Buffer) {
  const text = typeof raw === "string" ? raw : raw.toString("utf8");
  if (ws.data.oai?.readyState === 1) ws.data.oai.send(text);
}
```

## Step 4 — Native pub/sub for fan-out

Bun's `subscribe` / `publish` is built into the WebSocket — no Redis needed for small fleets:

```typescript
// In another route, push system messages to all connected sessions:
server.publish("call:" + sessionId, JSON.stringify({
  type: "conversation.item.create",
  item: { type: "message", role: "system", content: [{ type: "input_text", text: "Tool call done" }]}
}));
```

## Step 5 — Binary audio framing

Some clients send raw binary; Realtime expects base64 strings. Convert in the message handler:

```typescript
if (raw instanceof Buffer || raw instanceof Uint8Array) {
  ws.data.oai.send(JSON.stringify({
    type: "input_audio_buffer.append",
    audio: Buffer.from(raw).toString("base64"),
  }));
}
```

## Step 6 — Add a per-IP rate limit

```typescript
const buckets = new Map();
function allow(ip: string, max = 5, windowMs = 60_000) {
  const now = Date.now();
  const b = buckets.get(ip) ?? { n: 0, reset: now + windowMs };
  if (now > b.reset) { b.n = 0; b.reset = now + windowMs; }
  b.n++;
  buckets.set(ip, b);
  return b.n <= max;
}
```

## Common pitfalls

- **Using `ws` module on Bun** — it works but you lose 4x performance vs native.
- **Mixing Buffer and string in one handler** — branch explicitly.
- **No `close` cleanup** — leaks OpenAI sockets and your bill explodes.
- **Not setting `maxPayloadLength`** — large audio frames get dropped.

## How CallSphere does this in production

CallSphere's lightweight tenant-isolation proxy is a Bun service in front of the FastAPI :8084 voice cluster. It validates JWTs, applies plan-tier rate limits ($149/$499/$1499 — see [pricing](/pricing)), and routes to the right vertical agent. 6 verticals, 90+ tools, HIPAA + SOC 2.

## FAQ

**Is Bun production-ready?** Yes — 1.0 shipped Sep 2023, stable for HTTP & WS workloads.

**Can I deploy on Vercel?** Vercel doesn't run Bun runtimes; use Fly.io, Railway, or your own host.

**What about TypeScript?** Bun runs TS natively; no `tsx` needed.

**Hot reload?** `bun --watch run server.ts`.

**Cluster mode?** `Bun.serve({ reusePort: true })` and run N copies behind a single port.

## Sources

- [Bun WebSockets docs](https://bun.com/docs/runtime/http/websockets)
- [Bun.serve reference](https://bun.com/reference/bun/WebSocket)
- [OneUptime — Bun WebSocket servers (2026)](https://oneuptime.com/blog/post/2026-01-31-bun-websocket-servers/view)
- [OpenAI Realtime WebSocket](https://developers.openai.com/api/docs/guides/realtime-websocket)

---

Source: https://callsphere.ai/blog/vw2h-build-bun-websocket-voice-agent-bun-serve
