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. Nowsinstall, 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
- Bun 1.1+ installed (
curl -fsSL https://bun.sh/install | bash). OPENAI_API_KEYset in env.- A static frontend that connects to
ws://localhost:8080/voice. - Familiarity with TypeScript and the WebSocket API.
bun init-ed project.
Architecture
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<SessionData, undefined>({ 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.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
```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:
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
```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<string, { n: number; reset: number }>(); 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
wsmodule on Bun — it works but you lose 4x performance vs native. - Mixing Buffer and string in one handler — branch explicitly.
- No
closecleanup — 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), 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
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.