WebSocket and realtime
Connection management, pub/sub, backpressure, and keepalive with Bun.serve's native WebSocket
Last updated on
When to use it
- Chat, collaborative editing, live dashboards, agent progress streaming: Bun's native WebSocket is a first-class choice.
- Need Socket.IO room semantics, auto-reconnect, and fallbacks: the
wsor Socket.IO packages also run on Bun, but broadcasting is then up to the client library — nothing replacesserver.publish's native pub/sub. - Targeting Lambda, Cloudflare Workers, and similar platforms: this page's shape doesn't apply — see Serverless and edge.
Minimal echo server
const server = Bun.serve({
port: 3000,
fetch(req, server) {
if (server.upgrade(req)) return; // upgraded; no Response to return
return new Response('WebSocket endpoint', { status: 426 });
},
websocket: {
open(ws) {
ws.send('welcome');
},
message(ws, message) {
ws.send(message); // echo
},
close(ws, code, reason) {
console.log('closed', code, reason);
},
},
});Auth and connection context
The upgrade starts as a plain HTTP request — authenticate before upgrading; inject context via data, readable in every handler afterwards:
// Your project's auth: validate the Cookie/Token, return the user ID or null
function authenticate(req: Request): string | null {
return req.headers.get('x-demo-user'); // demo only; use real session validation in production
}
Bun.serve<{ userId: string }>({
fetch(req, server) {
const userId = authenticate(req);
if (!userId) {
return new Response('unauthorized', { status: 401 });
}
if (server.upgrade(req, { data: { userId } })) return;
return new Response('WebSocket endpoint', { status: 426 });
},
websocket: {
// the data property types ws.data across handlers
data: {} as { userId: string },
open(ws) {
ws.subscribe(`user:${ws.data.userId}`);
},
message(ws, message) { /* ... */ },
},
});Pub/Sub
| API | Semantics |
|---|---|
ws.subscribe(topic) / ws.unsubscribe(topic) | Manage one connection's topic subscriptions |
ws.publish(topic, data) | Send to all subscribers of the topic except self |
server.publish(topic, data) | Send to all subscribers of the topic including self |
ws.send(data) | Send to this connection only; returns bytes sent |
Always check ws.send's return value: 0 means dropped due to a connection problem, -1 means enqueued but backpressured. For high-frequency pushes, use backpressureLimit and closeOnBackpressureLimit to choose between buffering and disconnecting slow consumers — don't back slow clients with unbounded memory.
In-memory broadcast is per-process
server.publish pub/sub lives in the current process's memory. With multiple replicas, cross-replica messages need an external bus such as Redis Pub/Sub — see the Redis boundaries in Data and storage.
Idle timeout and keepalive
Bun closes WebSocket connections idle for 120 seconds by default (idleTimeout is configurable), but "idle" includes protocol-level pings: Bun defaults to sendPings: true, sends pings automatically, and standard clients answer pongs automatically — so a healthy receive-only connection is not dropped for silence. The cases you actually need to handle:
- Proxy links have their own idle timeouts: Nginx, Cloudflare, and platform LBs each cut idle connections, so the application layer should still design a 30–60s heartbeat and auto-reconnect around the shortest link;
- Zombie peers: with half-open TCP connections even pings may go unanswered —
idleTimeoutis the backstop, and clients must reconnect and restore subscriptions; - Only with
sendPingsdisabled or non-standard clients do you need periodicws.sendkeepalives of your own.
Production boundaries
- Cap single-message size with
maxPayloadLength; validate message bodies as untrusted input. - Log auth outcomes for upgrade requests, never tokens.
- On graceful shutdown, actively
ws.close()connections and stop accepting new upgrades — pair with the SIGTERM flow in Containers and Kubernetes. - Load-test connection count, messages per second, broadcast fan-out, and memory — not single-connection echo latency.
Acceptance
- Unauthenticated requests get 401; no anonymous post-upgrade connections exist;
- Slow consumers trigger the backpressure policy without memory growing with connections;
- Clients auto-reconnect and restore subscriptions across the 120s idle boundary;
- Multi-replica setups receive cross-replica messages through the external bus.
Official references: Bun WebSocket, Bun.serve.