Protocol-agnostic Bun-native WebSocket relay server with room-based architecture, binary protocol (msgpack), and per-client rate limiting. 5-8x faster than Node.js ws — same protocol, same clients.
- Bun-native WebSocket — Uses
Bun.serve()built-in WebSocket (5-8x faster than Node.jsws) - Room-based architecture —
/ws/:roomIdwith auto-created rooms and configurable player caps - Protocol-agnostic relay — Server relays any msgpack message between peers without inspecting payloads
- Binary protocol (msgpack) — ~40% smaller payloads than JSON
- Instant relay — Messages forwarded immediately to peers (no server-side batching)
- Bun pub/sub — Built-in topic-based broadcasting for efficient room messages
- Zero-allocation per-connection state —
ws.datapattern for per-client metadata - Per-client rate limiting — Sliding window algorithm
- KeepAlive — Bun's built-in ping/pong with configurable idle timeout
- Origin allowlist — Configurable CORS protection
- Health + Metrics endpoints —
/healthand/metricsfor monitoring and autoscaling - Production Dockerfile — Multi-stage (oven/bun:1-alpine), non-root user, HEALTHCHECK
# Install dependencies
bun install
# Development (with hot reload)
bun run dev
# Production
bun src/index.tsThis repo includes a tiny browser demo at examples/browser-demo.html that lets you connect two tabs and see relay messages in real time.
- Start the server:
bun run dev- Serve the demo page (any static server works):
cd examples
python3 -m http.server 3000- Open
http://localhost:3000/browser-demo.htmlin two tabs. - Click Connect in both tabs (defaults to
ws://localhost:8080/ws/lobby). - Type a message and click Send — the other tab will receive a
relay.
# Build and run
docker compose up --build
# Or manually
docker build -t bun-ws-gameserver .
docker run -p 8080:8080 bun-ws-gameserver| Variable | Default | Description |
|---|---|---|
PORT |
8080 |
Server listen port |
ALLOWED_ORIGINS |
* |
Comma-separated allowed origins |
KEEPALIVE_MS |
30000 |
Idle timeout (ms) |
MAX_MESSAGES_PER_SECOND |
60 |
Per-client rate limit |
MAX_PLAYERS_PER_ROOM |
50 |
Room capacity |
node-ws-gameserver, bun-ws-gameserver, and cloudflare-ws-gameserver use the same msgpack binary relay protocol, so clients are backend-agnostic.
The server is protocol-agnostic — it manages rooms and connections, but treats game data as opaque payloads. Any client that speaks msgpack can use it: multiplayer games, collaborative tools, IoT dashboards, chat apps, etc.
- Client connects to
ws://host/ws/:roomId - Server auto-assigns a
playerIdand sendswelcomewith list of existing peers - Client sends any msgpack messages — server wraps each in a
relayenvelope and forwards to all other peers - When peers join/leave, server notifies all remaining peers
// Sent on connect
{ type: "welcome", playerId: string, peers: string[] }
// Peer lifecycle
{ type: "peer_joined", peerId: string }
{ type: "peer_left", peerId: string }
// Relayed game data from another peer (data is passed through untouched)
{ type: "relay", from: string, data: any }
// Keepalive response
{ type: "pong", nonce: string, serverTime: number }
// Errors (rate limit, room full, bad message)
{ type: "error", code: string, message: string }// Optional keepalive
{ type: "ping", nonce: string }
// ANYTHING ELSE is relayed to all other peers in the room.
// The server does not inspect or validate your game data.
// Examples:
{ type: "position", x: 1.5, y: 0, z: -3.2 }
{ type: "chat", text: "hello" }
{ type: "snapshot", pos: [0, 1, 0], rotY: 3.14, locomotion: "run" }import { encode, decode } from '@msgpack/msgpack';
const ws = new WebSocket('ws://localhost:8080/ws/lobby');
ws.binaryType = 'arraybuffer';
let myId: string;
ws.onmessage = (event) => {
const msg = decode(new Uint8Array(event.data));
switch (msg.type) {
case 'welcome':
myId = msg.playerId;
console.log(`Joined as ${myId}, peers:`, msg.peers);
break;
case 'peer_joined':
console.log(`${msg.peerId} joined`);
break;
case 'peer_left':
console.log(`${msg.peerId} left`);
break;
case 'relay':
// msg.from = peer ID, msg.data = whatever they sent
handlePeerData(msg.from, msg.data);
break;
}
};
// Send your game state (any shape you want)
setInterval(() => {
ws.send(encode({
type: 'position',
x: Math.random() * 10,
y: 0,
z: Math.random() * 10,
}));
}, 50);Node.js (ws) |
Bun (native) | |
|---|---|---|
| WebSocket throughput | ~50k msg/s | ~400k msg/s |
| HTTP + WS server | Separate setup | Single Bun.serve() |
| Per-connection state | WeakMap lookup | ws.data (zero-alloc) |
| Broadcasting | Manual loop | Built-in pub/sub topics |
| Startup time | ~200ms | ~20ms |
Same protocol, same clients, same API — just faster.
| Path | Method | Description |
|---|---|---|
/ws/:roomId |
WS | WebSocket connection (default room: "lobby") |
/health |
GET | Health check — status, rooms, connections, uptime |
/metrics |
GET | Detailed metrics — memory, messages/sec per room |
Click the deploy button at the top, or go to app.alternatefutures.ai — select this template and deploy to decentralized cloud in one click.
- Fork this repo
- Connect to Railway
- Deploy — Railway reads
railway.tomlautomatically
docker build --platform linux/amd64 -t bun-ws-gameserver .
docker run -p 8080:8080 -e PORT=8080 bun-ws-gameserver| Repo | Runtime | Deploy Target |
|---|---|---|
node-ws-gameserver |
Node.js 20 + ws |
Docker, Railway, DePIN, any host |
| bun-ws-gameserver | Bun native WS | Docker, Railway, DePIN, any host |
cloudflare-ws-gameserver |
Cloudflare Workers + DO | Cloudflare edge (global) |
All three implement the same msgpack relay protocol. Clients connect to any of them by changing the server URL.
GPL-3.0-only