Skip to content
NEWKreativwit is open for business • Now onboarding new clients
Kreativwit
7 MIN READBUN 1.1 + NODE 22LINUX 6.x KERNEL

Sub-10ms WebSockets with Node.js & Bun: Kernel Tuning, epoll, and Memory Slicing

Profiling 500,000 concurrent socket connections. How adjusting TCP window buffers, utilizing Bun raw sockets, and eliminating V8 GC pauses halved our server fleet costs.

Priya Sundaram
Staff ML Infra Engineer • Kreativwit Core
REVISION 1.3
SECTION 01 — INITIAL DIAGNOSTICS

Where 14.8 KB per Connection Goes

Our realtime gateway held 500,000 idle-but-live WebSocket connections across a fleet of Node.js servers. The fleet was sized by memory, not CPU: each connection cost 14.8 KB of resident memory, so a 16 GB node topped out around 1 million sockets in theory and 600,000 in practice.

Heap snapshots showed the cost was spread thin: per-socket buffers, an EventEmitter per connection, closure-captured state and a framing object allocated for every inbound message. None of it was a leak, which is exactly why it never showed up in a profiler.

“We were not out of CPU. We were paying a memory tax on every socket just to keep it politely idle.”

— Priya Sundaram, Gateway Postmortem Log
SECTION 02 — KERNEL & TRANSPORT

TCP Buffers, epoll and File Descriptors

Default Linux TCP buffers are sized for throughput, not for a million mostly-silent sockets. Shrinking the per-socket read and write buffers returned hundreds of megabytes per node, and raising the file descriptor ceiling removed an entire class of EMFILE incidents.

sysctl.d/99-gateway.conf • Linux kernel parametersAPPLIED ON EVERY GATEWAY NODE
# fewer bytes pinned per idle connectionnet.ipv4.tcp_rmem = 4096 8192 262144net.ipv4.tcp_wmem = 4096 8192 262144 # room for a million socketsfs.file-max = 2097152net.core.somaxconn = 65535net.ipv4.ip_local_port_range = 1024 65535
IDLE-SOCKET FRIENDLYNO THROUGHPUT LOSS ON HOT PATHS

With tuned buffers the same workload fit in roughly 9 KB per socket on Node.js. The remaining gap was userland: the ws library and V8 itself.

SECTION 03 — RUNTIME SWAP

Bun Raw Sockets and Zero GC Pressure

Bun's native WebSocket server keeps connection state in native memory and hands your handler a single shared object, instead of allocating an emitter and closures per socket. Moving the gateway over took the per-connection cost from 14.8 KB to 2.1 KB, and removed the garbage-collector pauses that had been inflating our tail latency.

gateway.ts • TypeScript • BunONE HANDLER SHARED BY EVERY SOCKET
Bun.serve({  port: 8080,  fetch(req, server) {    return server.upgrade(req) ? undefined : new Response(null, { status: 426 });  },  websocket: {    perMessageDeflate: false,    message(ws, data) {      ws.send(data);    },  },});
NO PER-CONNECTION CLOSURESBACKPRESSURE AWARE
SECTION 04 — ENGINEERING DIRECTIVES

Checklist for Million-Socket Gateways

If you run a realtime gateway that is sized by memory rather than CPU, audit these checks before adding nodes:

  1. Measure Memory Per Connection First

    Divide resident memory by live sockets under a soak test. It is the number your fleet size is really built on.

  2. Shrink TCP Buffers For Idle Sockets

    Tune tcp_rmem and tcp_wmem for mostly-silent connections and keep large buffers only where throughput needs them.

  3. Raise File Descriptor And Port Limits

    Set fs.file-max, somaxconn and the ephemeral port range before the load test, not after the incident.

  4. Avoid Per-Connection Allocations

    Share one handler across sockets. Per-socket emitters and closures are the hidden cost at this scale.

  5. Disable Compression On Chatty Small Frames

    perMessageDeflate buys little on tiny frames and costs memory and CPU on every connection.

  6. Soak For Hours, Not Minutes

    Latency tails from garbage collection and kernel memory compaction only appear after sustained load.

Live cluster telemetry
SOCKETS:
500,000
RUNTIME:
Bun 1.1
BASELINE:
Node 22 + ws
NODE SIZE:
8 vCPU / 16 GB
SOAK TEST:
6 hours

Running a Realtime Gateway?

Book a 30m architectural review with Priya Sundaram and the Kreativwit Core team.

SCHEDULE REVIEW