Bun
Diminuendo runs on Bun — not as a fashion choice, but because Bun provides three load-bearing capabilities that would each require a separate dependency (and a separate failure mode) in Node.js: a native WebSocket server with built-in pub/sub, native SQLite with prepared statements and WAL mode, and Web Workers for offloading I/O. These are not nice-to-haves. They are the infrastructure on which the gateway’s performance and operational simplicity directly depend.Why Bun over Node.js
The decision to use Bun over Node.js was not an optimization for throughput alone — it was an optimization for architectural surface area. Each capability that Bun provides natively is one fewer external dependency to version, configure, monitor, and debug in production.| Capability | Node.js | Bun | Architectural Impact |
|---|---|---|---|
| WebSocket server | ws package, no pub/sub | Bun.serve() with topic-based pub/sub | Eliminates external message broker for single-instance |
| SQLite | better-sqlite3 (native addon) | bun:sqlite (built-in) | No native compilation, no node-gyp, no platform-specific binaries |
| Startup time | 500ms-2s for typical services | < 200ms | Rapid restart cycles, minimal downtime during deployment |
| Web Workers | worker_threads | Native Worker with postMessage | I/O offloading for SQLite without third-party threading libraries |
package.json contains zero infrastructure dependencies. There is no ws, no better-sqlite3, no ioredis, no pg. The runtime is the infrastructure.
Bun.serve: One Port, Two Protocols
Bun.serve() handles both HTTP and WebSocket traffic on a single port. There is no separate WebSocket server to configure, no port allocation to manage, no reverse proxy to wire up for protocol switching.
WebSocket Pub/Sub
Bun’s WebSocket implementation includes a topic-based publish/subscribe system that operates entirely in-process. There is no external broker, no serialization to Redis, no network hop between publish and delivery.Topic Naming
Diminuendo uses two topic namespaces:session:{sessionId}— all events for a specific session. Every client that has joined a session subscribes to its topic.tenant:{tenantId}:sessions— tenant-wide notifications. Session creation, deletion, archival, and rename events are broadcast here so that every authenticated client in the tenant can update its session list.
The Broadcaster Abstraction
TheBroadcaster service wraps Bun’s raw pub/sub in an Effect Layer, providing typed methods for session events, tenant events, and shutdown broadcasts. This abstraction serves two purposes: it makes pub/sub testable (tests provide a mock Broadcaster) and it prepares for future multi-instance deployments where the pub/sub layer would need to bridge across processes.
The Broadcaster tracks every topic that has received at least one publish. On shutdown, it iterates this set and publishes a server_shutdown event to each, ensuring every connected client receives orderly notification before the process exits.
Bun’s WebSocket pub/sub is per-process. For multi-instance deployments where a session’s subscribers might span gateway instances, a shared pub/sub layer (Redis Streams, NATS, or similar) would be needed. The Broadcaster abstraction makes this swap a Layer replacement, not a rewrite.
bun:sqlite
Bun ships with a built-in SQLite binding (bun:sqlite) that operates synchronously and in-process. There is no network round-trip, no connection pool, no serialization overhead, no external process to manage. A query executes as a function call — the same address space, the same event loop tick.
Key Characteristics
- Synchronous API —
db.query("SELECT ...").all()returns rows immediately. Noawait, no callback, no promise. In the context of Web Workers (where blocking is acceptable), this is the optimal execution model. - Prepared statements —
db.query(sql)compiles the SQL once and returns a reusableStatementobject. Diminuendo caches these in aWeakMapkeyed by database handle, so hot-path queries (event inserts, message fetches) never recompile. - WAL mode — Write-Ahead Logging allows concurrent readers while a single writer commits transactions. The two-worker architecture exploits this: the reader worker never blocks the writer, and the writer never blocks readers.
- Zero serialization — rows are returned as JavaScript objects directly from the native binding. There is no JSON parse step, no ORM hydration, no ResultSet wrapper. The data arrives in the shape the caller needs.
Performance Impact
The in-process nature ofbun:sqlite eliminates the dominant cost in traditional database architectures: network latency. A PostgreSQL query over TCP adds 1-5ms of latency per round-trip (more under load, more in cloud environments with network virtualization). An in-process SQLite query adds microseconds. For a gateway that executes multiple database operations per client message, this difference compounds:
| Operation | bun:sqlite | PostgreSQL (TCP) | Delta |
|---|---|---|---|
| Session creation | ~0.3ms | ~10-20ms | 30-60x |
| Event insert (batched) | ~0.05ms per row | ~2-5ms per row | 40-100x |
| History query (100 rows) | ~0.5ms | ~5-15ms | 10-30x |
Web Workers for SQLite I/O
On the main thread, Bun’s event loop handles WebSocket I/O, JSON parsing, and Effect fiber scheduling. Synchronous SQLite writes on the main thread would block event delivery to all connected clients for the duration of the transaction — an unacceptable trade-off for a real-time streaming gateway. Diminuendo offloads all SQLite I/O to two dedicated Bun Web Workers:- The writer worker batches incoming commands and flushes on a 50ms timer or at 100 commands, whichever comes first. Writes never block the main thread.
- The reader worker handles
SELECTqueries on a separate thread. WAL mode ensures readers never block the writer.
postMessage with structured cloning. The WorkerManager Effect Layer abstracts this boundary entirely — consumers call typed methods (write(), readHistory(), flush()) without awareness that structured messages cross thread boundaries.
For the full details of the batching strategy, LRU cache sizing, shutdown protocol, and prepared statement management, see Multi-Worker SQLite.
Trade-offs
Every runtime choice carries obligations. Bun’s are worth acknowledging:Smaller ecosystem
Smaller ecosystem
Bun’s npm compatibility is high but not perfect. Some packages with native addons do not compile cleanly. In practice, Diminuendo’s zero-external-dependency architecture sidesteps this issue entirely — the gateway does not use
ws, better-sqlite3, or any other native addon. But teams extending the gateway with third-party packages should verify Bun compatibility.Fewer production battle-scars
Fewer production battle-scars
Node.js has been subjected to over a decade of adversarial workloads at planetary scale. Bun has not. Edge cases in memory management, garbage collection under pressure, and long-running process stability are less thoroughly explored. Diminuendo mitigates this with bounded resource caches (LRU eviction), periodic cleanup of rate limiter state, and a force-exit timer on shutdown.
Worker semantics differ from Node.js
Worker semantics differ from Node.js
Bun’s Web Workers differ from Node.js
worker_threads in subtle ways — particularly around SharedArrayBuffer support and message port lifecycle. The multi-worker SQLite architecture was designed with Bun-specific semantics in mind. Porting to Node.js would require adjustments to the worker spawn and communication patterns.Performance Summary
Benchmarks against a Next.js-based gateway (Crescendo) connecting to the same Podium and Ensemble backends demonstrate the compound effect of Bun’s native primitives:| Metric | Diminuendo | Crescendo | Speedup |
|---|---|---|---|
| Health check p50 | 0.6ms | 5.0ms | 8.4x |
| Health check RPS | 10,390 | 291 | 35.7x |
| Connection + auth p50 | 0.4ms | 5.5ms | 15.7x |
| Session creation p50 | 0.6ms | 17.7ms | 27.6x |
| Session creation stddev | 0.1ms | 8.9ms | 89x less variance |