@rvncom/socketio-bun-engine
v1.1.5
Published
Engine.IO server implementation for Bun runtime
Maintainers
Readme
@rvncom/socketio-bun-engine
Engine.IO server implementation for the Bun runtime. Provides native WebSocket and HTTP long-polling transports for Socket.IO.
Fork of @socket.io/bun-engine with bug fixes, improved API, and active maintenance.
Installation
bun add @rvncom/socketio-bun-engineUsage
import { Server as Engine } from "@rvncom/socketio-bun-engine";
import { Server } from "socket.io";
const engine = new Engine({
path: "/socket.io/",
});
const io = new Server();
io.bind(engine);
io.on("connection", (socket) => {
// ...
});
export default {
port: 3000,
...engine.handler(),
};You can also use engine.handleRequest() directly for custom routing:
Bun.serve({
port: 3000,
fetch(req, server) {
const url = new URL(req.url);
if (url.pathname === "/health") {
return new Response(
JSON.stringify({ status: "ok", connections: engine.clientsCount }),
{
headers: { "Content-Type": "application/json" },
},
);
}
return engine.handleRequest(req, server);
},
websocket: engine.handler().websocket,
});Options
path
Default: /engine.io/
The path to handle on the server side. Must match the client configuration.
pingTimeout
Default: 20000
Milliseconds without a pong packet before considering the connection closed.
pingInterval
Default: 25000
Milliseconds between ping packets sent by the server.
upgradeTimeout
Default: 10000
Milliseconds before an uncompleted transport upgrade is cancelled.
maxHttpBufferSize
Default: 1e6 (1 MB)
Maximum message size in bytes before closing the session.
pollingTimeout
Default: 60000 (60 seconds)
Milliseconds before a pending polling request times out. If the client doesn't send a new poll within this window, the transport is closed.
maxClients
Default: 0 (unlimited)
Maximum number of concurrent clients. New connections are rejected with HTTP 503 when the limit is reached.
maxHandshakesPerSecond
Default: 0 (unlimited)
Maximum number of new handshakes allowed per second (global). Excess handshakes receive HTTP 429 with Retry-After: 1. Protects against connection flood attacks.
const engine = new Engine({
maxHandshakesPerSecond: 100, // max 100 new connections/sec
});maxHandshakesPerIp
Default: 0 (unlimited)
Maximum number of new handshakes per second from a single IP address. Excess receive HTTP 429. Combine with maxHandshakesPerSecond for layered protection — a single noisy IP can't exhaust the global budget for legitimate clients.
const engine = new Engine({
maxHandshakesPerSecond: 1000, // global
maxHandshakesPerIp: 20, // per-IP
});backpressureThreshold
Default: 1048576 (1 MB)
WebSocket send buffer threshold in bytes. When getBufferedAmount() exceeds this value, writes are paused automatically and resumed when the buffer drains. Set to 0 to disable.
rateLimit
Per-socket message rate limiting. Disabled by default.
const engine = new Engine({
rateLimit: {
maxMessages: 100, // max messages per window
windowMs: 1000, // window duration in ms
},
});
engine.on("connection", (socket) => {
socket.on("rateLimited", () => {
console.log(`Socket ${socket.id} rate limited`);
});
});perMessageDeflate
Default: false
Enable WebSocket per-message deflate compression (RFC 7692). Pass true for defaults or a Bun.WebSocketPerMessageDeflateOptions object for fine-grained control. Provides 50-70% bandwidth savings for text-heavy payloads.
const engine = new Engine({
perMessageDeflate: true,
});enableMetrics
Default: false
Controls whether per-message byte counting (bytesReceived, bytesSent, avgRtt, upgrades) is active from the start. When false, these metrics activate lazily on first server.metrics access. Connection and disconnection counters are always tracked regardless of this option.
const engine = new Engine({
enableMetrics: true, // attach byte-counting listeners immediately
});degradationThreshold
Default: 0 (disabled)
Fraction (0–1) of maxClients at which graceful degradation activates. Requires maxClients > 0. When active:
- New polling connections are rejected (WebSocket only, returns 503)
- New connections get doubled
pingIntervalto reduce heartbeat overhead
const engine = new Engine({
maxClients: 10000,
degradationThreshold: 0.8, // degrade at 8000+ clients
});
engine.on("degradation", ({ active, clients }) => {
console.log(`Degradation ${active ? "ON" : "OFF"} at ${clients} clients`);
});allowRequest
A function that receives the handshake/upgrade request and can reject it:
const engine = new Engine({
allowRequest: (req, server) => {
return Promise.reject("not allowed");
},
});cors
Cross-Origin Resource Sharing options:
const engine = new Engine({
cors: {
origin: ["https://example.com"],
allowedHeaders: ["my-header"],
credentials: true,
},
});editHandshakeHeaders
Edit response headers for the handshake request:
const engine = new Engine({
editHandshakeHeaders: (responseHeaders, req, server) => {
responseHeaders.set("set-cookie", "sid=1234");
},
});editResponseHeaders
Edit response headers for all requests:
const engine = new Engine({
editResponseHeaders: (responseHeaders, req, server) => {
responseHeaders.set("my-header", "abcd");
},
});Metrics
Built-in server metrics with zero dependencies. Per-message byte counting is lazy by default — counters activate on first server.metrics access (or immediately with enableMetrics: true). Connection/disconnection counters are always active.
const snapshot = engine.metrics;
// {
// connections: 150, // total opened (cumulative)
// disconnections: 12, // total closed
// activeConnections: 138, // currently connected
// upgrades: 130, // polling → websocket
// bytesReceived: 524288,
// bytesSent: 1048576,
// errors: 2,
// avgRtt: 14, // average round-trip time (ms, max 1000 samples)
// pollingCount: 8, // currently connected polling transports
// websocketCount: 130 // currently connected websocket transports
// }Per-socket RTT is also available:
engine.on("connection", (socket) => {
socket.on("heartbeat", () => {
console.log(`RTT: ${socket.rtt}ms`);
});
});API
server.clientsCount
Number of currently connected clients.
server.metrics
Returns a MetricsSnapshot object with server-wide counters.
server.sockets
Iterator over all connected Socket instances.
server.getSocket(id)
Look up a specific socket by session ID.
server.use(middleware)
Registers a middleware function run on each handshake before allowRequest. Middlewares are invoked in registration order. Call next() to continue, or next(err) to reject the handshake with HTTP 403.
engine.use((req, server, next) => {
const token = new URL(req.url).searchParams.get("token");
if (!token) return next(new Error("missing token"));
// synchronous or async work, then continue
next();
});
engine.use(async (req, server, next) => {
await checkAuth(req);
next();
});socket.remoteAddress
Resolved client IP address (string) from Bun.Server.requestIP(). May be undefined if Bun can't resolve it (some proxy setups).
server.broadcast(data)
Sends a message to all connected sockets. The packet is encoded once and sent as pre-encoded data to WebSocket transports (zero-copy). Polling transports fall back to the normal path.
server.broadcastExcept(excludeId, data)
Sends a message to all connected sockets except the one with the given id. Same zero-copy optimization as broadcast().
server.degraded
Returns true if the server is currently in degraded mode.
server.shutdown(opts?)
Gracefully shuts down the server. Stops accepting new connections (returns 503), sends close to all existing clients, and resolves when all are disconnected or after the timeout.
await engine.shutdown({ timeout: 10000 }); // default: 10s
engine.on("shutdown", () => {
console.log("Server shut down");
});Options:
timeout(default:10000): Maximum time in milliseconds to wait for clients to disconnect before force-closing.
server.draining
Returns true after shutdown() has been called.
server.close()
Returns a Promise<void> that resolves when all clients have disconnected.
socket.bytesSent / socket.bytesReceived
Cumulative byte counters for this socket's message traffic. Counts payload bytes only (excludes protocol framing).
socket.messagesSent / socket.messagesReceived
Cumulative message counters for this socket.
socket.connectedAt
Timestamp (Date.now()) of when the socket was created. Useful for computing session duration:
engine.on("connection", (socket) => {
socket.on("close", () => {
const duration = Date.now() - socket.connectedAt;
console.log(
`Socket ${socket.id}: ${socket.messagesSent} sent, ${socket.bytesReceived} bytes recv, ${duration}ms`,
);
});
});Debugging
Enable debug logs with the NODE_DEBUG environment variable:
# All engine.io logs
NODE_DEBUG=engine.io bun run server.ts
# Specific subsystems
NODE_DEBUG=engine.io:socket bun run server.ts # Socket logs only
NODE_DEBUG=engine.io:websocket bun run server.ts # WebSocket logs only
NODE_DEBUG=engine.io:polling bun run server.ts # Polling logs only
# Multiple subsystems
NODE_DEBUG=engine.io:socket,engine.io:websocket bun run server.tsRequirements
- Bun >= 1.0.0
- TypeScript >= 5.9.2 (peer dependency)
Benchmarks
Benchmarked on GitHub Actions (
ubuntu-latest), v1.1.4 vs@socket.io/bun-engine. Full report.
| Metric | vs upstream | @rvncom | @socket.io | |--------|------------|---------|------------| | Throughput | 1.1x faster | 246,305 msg/s | 215,517 msg/s | | Connections | 5% slower | 874 conn/s | 924 conn/s | | Latency (p95) | ~same | 1.6 ms | 1.7 ms |
