Originally published at kunalganglani.com — read it there for inline code, hero image, and live links.
You’re not deciding between Bun and Node for “developer happiness.” You’re deciding whether your next service will autoscale cleanly, fit 2× more pods per node, and keep 50,000 WebSocket connections from turning into a permanent CPU tax.
If you’re searching bun vs node performance 2026, you probably already know the hot takes. Bun is “fast.” Node is “mature.” Cool. That doesn’t help when you’re staring at p95 cold starts, tight memory limits, and a real-time feature that suddenly made your fleet stateful.
Here’s what actually matters in 2026:
- Cold start is the most honest benchmark for modern infra. It’s what autoscaling and serverless feel like when nobody’s watching.
-
Memory isn’t
heapUsed. RSS decides container density and whether Kubernetes evicts you. - WebSockets are where runtimes get exposed. Churn, backpressure, and broadcast patterns punish sloppy event loops.
- Bun can be a clean win, but only in specific service shapes. Greenfield HTTP + WS services benefit the most.
- Node still wins by default when your app depends on the ecosystem. LTS cadence, native addons, and tooling depth matter in boring but expensive ways.
If your workload lives on autoscaling and long-lived connections, runtime choice is an infrastructure decision, not a language decision.
What is Bun vs Node performance 2026?
Bun vs Node performance 2026 is the real-world comparison of Bun’s JavaScriptCore-based runtime and Node.js’s V8-based runtime across the metrics that actually hit production: cold start latency, memory footprint (RSS), and WebSocket throughput/connection handling.
When people argue about “performance,” they usually mean throughput on a microbenchmark. In 2026, most teams feel performance as:
- Tail latency during scale events (cold start p95)
- Binpacking pressure (RSS per replica)
- Persistent connection cost (WebSockets: CPU per 10k conns, fan-out capacity)
That’s the shape of this post.
The benchmark harness I’d trust (and what “fair” means)
Most runtime shootouts cheat accidentally.
They benchmark “hello world” with no reverse proxy, no TLS, no connection limits, no warmup phase. Then they act shocked when the results don’t map to production. I don’t care if a runtime wins a toy test by 12%. I care if it lets me run fewer nodes or stop flirting with OOMKills.
Here’s the harness design that stays defensible.
Workloads (three, because production has three kinds of pain)
- Cold start (containers/serverless style): start process, bind port, serve first request.
- Steady-state memory: idle RSS and RSS under load.
-
WebSockets:
- Connection churn: connect/disconnect loops (LB reconnections, mobile clients)
- Long-lived connections: 5–60 minutes stable
- Echo vs broadcast fan-out: “send back to caller” is not what chat systems do
Concrete defaults I’d use:
- HTTP JSON endpoint: 1 KB JSON request, 2 KB JSON response, basic validation.
- WebSocket message size: 256 B, 1 KB, 8 KB (three tiers that match chat, presence, and “oops we shipped JSON over WS”).
- Rooms: 100 rooms, 1,000 rooms.
- Clients: 1,000 / 10,000 / 50,000 connections (stepwise, because failure modes change).
Fairness controls (the stuff everyone “forgets”)
If you don’t lock these down, you’re not benchmarking runtimes. You’re benchmarking your own sloppiness.
-
Same machine, same kernel, same ulimits. If one run gets a different
ulimit -n, your WebSocket result is a joke. -
CPU pinning: keep the server on the same cores (
taskseton Linux). Otherwise you’re measuring the scheduler and thermal noise. - Warmup phase: measure p50/p95 after a fixed warmup window. JIT and caches are real.
- Same TLS strategy: either terminate TLS at a reverse proxy for both, or do it in-process for both.
- Backpressure behavior: if the client can out-send the server, pick a consistent policy (drop, buffer, or slow) and document it.
Node gives you good primitives for this kind of instrumentation via perf_hooks and memory stats via process.memoryUsage() (see the official Node.js contributors). Bun’s runtime and server APIs are built around Bun.serve with integrated WebSocket upgrade handling, as documented by the Bun team.
If you want an external “sanity check” for throughput positioning, TechEmpower is useful. Just don’t pretend it’s your workload. Framework choice, DB access, and JSON behavior dominate. The TechEmpower suite is famous because it’s broad, not because it matches your app.
Here’s a current-year video that shows common benchmark patterns (and some pitfalls) in the wild. Watch it for context, not as a substitute for reproducibility:
[YOUTUBE:_iEaaNIjg7U|Deno vs. Node.js vs. Bun Performance & Comparison (2026)]
A small but important reproducibility detail
Cold start numbers are meaningless unless you say what you counted.
I recommend publishing two cold-start measurements:
- Cold start (image already present): excludes image pull time. Measures runtime + init + bind.
- Cold start (image pull included): includes pull. Measures your registry/network reality.
In Kubernetes, the first one influences HPA reaction time. The second one matters during cluster recovery, node churn, and “the registry is having a day.”
Results: what typically wins on cold start, memory, and WebSockets
I’m going to be blunt. I don’t trust your absolute numbers unless you ran them on your own infra. But I do trust directional differences when the harness is fair and the workload isn’t contrived.
So here’s the decision table I’d use for 2026, based on how these runtimes are built and what they optimize for.
| Metric (real app lens) | Bun (JavaScriptCore + Bun.serve) |
Node.js (V8 + ecosystem) | What I’d pick |
|---|---|---|---|
| Cold start p50/p95 (container/serverless) | Usually lower startup overhead. Single-binary story helps. | Improved over time, but heavier baseline, more moving parts. | Bun for spiky autoscale services. |
| Idle memory (RSS) | Often lower RSS for simple services. | Often higher RSS baseline, depends on flags and loaded modules. | Bun if you’re binpacking. |
| Memory under load (RSS + external) | Can look great until payloads/alloc patterns change. Watch cliffs. | Tooling to analyze memory is excellent (--inspect, heap snapshots). |
Node if you’re chasing leaks weekly. |
| WebSocket echo throughput | Strong built-in primitives for upgrade + WS handling. | Depends heavily on library (ws vs uWebSockets) and tuning. |
Tie. Choose based on your WS library comfort. |
| WebSocket broadcast fan-out | Can be very good if your server loop is tight and allocations are low. | You can win, but you’ll work for it. Backpressure patterns matter. | Bun for greenfield real-time. |
| Debugging/profiling maturity | Improving, but fewer battle-tested workflows. | Best-in-class ecosystem and “how to debug this at 3 a.m.” docs. | Node for org-scale services. |
Now the practical version.
Cold start: what to measure and what it means
Cold start is p95 user experience wearing an ops hat.
In 2026, scale events happen because:
- traffic spikes
- deployments roll
- a node gets recycled
- HPA decides your CPU target is being violated
If your runtime takes 300 ms longer to become ready, you rarely just “lose 300 ms.” You get queuing behind readiness. You get thundering-herd retries. You get that slightly panicky Slack thread where everyone blames the database because that’s the only graph they trust.
A good harness should report at least:
- p50 and p95 cold start (in milliseconds)
- time to first successful request after bind
- time to steady latency (after warmup), because first request is often a liar
And publish your environment:
- instance type / CPU model
- OS and kernel version
- Bun version and Node major
Bun markets itself as reducing startup time and memory via JavaScriptCore and an integrated toolchain. That matches the bet it’s making. Keep the runtime tight, make the default server path fast, ship the thing as a single executable when possible (Bun team). Node is still competitive. It’s just optimized for a different constraint set: stability, compatibility, and an ecosystem that’s basically its own economy.
Memory: RSS vs heapUsed vs external (and why you should care)
Most engineers quote heapUsed because it’s easy to print.
It’s also the wrong number to optimize if your real problem is “why can’t we fit more replicas on this node?”
What matters in containers:
- RSS: what the kernel thinks you’re using. This is what triggers OOM and eviction.
- heapUsed: what V8 thinks is on-heap.
- external: buffers, native allocations, and “stuff not counted in heapUsed.”
Node exposes these quickly with process.memoryUsage() and has genuinely good docs on memory profiling and diagnostics (Node.js contributors).
Bun’s memory story can look fantastic for simple HTTP services. The question I care about is: does it stay well-behaved when the workload is annoying?
- Does RSS climb after 10 minutes of WebSocket churn?
- Does it flatten after GC cycles?
- Does it drop after load stops?
If you run WebSockets at scale, you’re usually holding:
- socket buffers
- per-connection state
- room membership maps
- outbound queues
Those allocations are where runtimes show their personality.
WebSockets: echo is easy. broadcast is the benchmark.
Almost every “WebSocket benchmark” online is an echo test.
Echo tests are fine for validating overhead. They’re useless for sizing a chat system, a presence system, or anything where one message fans out to a bunch of sockets.
What you want is a broadcast fan-out test:
- N clients connected
- clients join rooms
- server broadcasts a message to a room
- measure messages delivered/sec and p95 delivery latency
Two patterns create very different failure modes:
- Connection churn: mobile networks, LB rebalancing, clients that reconnect on deploy.
- Long-lived steady connections: dashboards, chat, multiplayer.
Bun’s Bun.serve combines HTTP and WS upgrades in one primitive, which reduces glue code. Glue code is where you accidentally allocate, accidentally buffer, and accidentally never free (Bun team). In Node, you can absolutely build a great WS server. You just have more knobs to get wrong: library choice, upgrade handling, ping/pong strategy, backpressure policy.
If you want a sanity check: uWebSockets.js is often the “fast path” people reach for in Node, while ws is the default ergonomic choice. That gap alone can dwarf the Bun vs Node delta, which is why “runtime X is faster” is such a lazy take.
Deployment implications: turning benchmarks into dollars and incidents
This is where I stop caring about benchmark culture and start caring about consequences.
Benchmarks are only useful if you can turn them into:
- pods per node (binpacking)
- autoscale reaction time (cold start)
- tail latency under connection fan-out
Container density math (the boring answer that saves money)
Let’s say you run a 16 GiB node and reserve 2 GiB for system overhead. You have 14 GiB for workloads.
If your runtime idles at:
- 200 MiB RSS per replica, you can fit ~70 replicas.
- 350 MiB RSS per replica, you can fit ~40 replicas.
That’s a 1.75× difference in density.
Then reality shows up. Sidecars. Agents. “Temporary” debug flags that never get removed. Suddenly your cluster upgrade budget is a negotiation.
Autoscaling and cold start: p95 is the tax you pay during deploys
If your p95 cold start is even 500 ms worse, you don’t just lose 500 ms.
You get:
- longer readiness
- bigger queues
- more time in overload
- more retries
That’s how you get cascading latency incidents that present as “the database is slow” but started as “pods took too long to come up.”
If you care about this, you’ll probably also care about system-level observability. I’ve written about how to get high-signal data without drowning in telemetry in 7-Step Plan: eBPF Observability Without Sidecars on Kubernetes and how to avoid p99 cliffs in Transparent Huge Pages + Postgres: Stop P99 Latency Cliffs [2026].
WebSocket deployments: load balancers, sticky routing, and failure domains
The operational trap with WebSockets is that the bottleneck moves.
You’ll run into:
- LB connection limits (count your max connections per target)
- sticky sessions or consistent hashing (rooms need locality)
- broadcast topology (single node vs pub/sub)
If you’re doing fan-out, you’re doing distributed systems. Pretending otherwise is how you end up debugging ghost disconnects at 2 a.m.
If you’re running this on Kubernetes, the boring rules still apply:
- set realistic
readinessProbeandstartupProbe - tune
net.core.somaxconn - raise
ulimit -n - measure connection churn explicitly
When I’d choose Bun vs when I’d stick with Node
Here’s my stance for 2026.
Bun is a safe win when…
- You’re building a greenfield HTTP + WebSocket service.
- You care about cold start and memory footprint more than obscure ecosystem dependencies.
- You want a simpler runtime story (single toolchain, fewer moving parts).
- Your WebSocket workload is heavy on concurrent connections and you can keep the app logic lean.
Stick with Node when…
- You rely on a mature ecosystem: frameworks, observability agents, native modules.
- You need predictable release and support expectations (LTS cadence is a real organizational feature).
- Your team’s debugging muscle memory is Node-shaped, and you don’t want to pay the transition tax.
- You ship anything involving native addons or complex deployment constraints.
This is one of those cases where the boring answer is actually the right one. Most teams should not migrate a stable Node system just because Bun wins a benchmark. You migrate when the infra math is screaming and you’ve run the harness enough times to trust it.
If you’re already deep in JavaScript/TypeScript architecture decisions, you might also like TypeScript vs JavaScript 2026: Type Safety Finally Worth the Cost? and Hono vs Express in 2026: Which API Framework Actually Wins?.
JavaScriptCore vs V8: why runtime architecture shows up as infra behavior
The engine matters. Just not in the simplistic “JSC fast, V8 slow” way people argue about online.
Bun is powered by JavaScriptCore and is built as an integrated toolkit. Node is powered by V8 and sits at the center of a massive ecosystem.
In practice, the engine difference shows up when:
- you care about startup behavior and baseline overhead
- your app allocates aggressively (buffers, JSON parsing, WS fan-out)
- you need profiling and diagnostics at depth
Node can go very deep on diagnostics and memory profiling. Bun’s bet is that a tighter runtime plus a tight server primitive keeps the hot path simpler and cheaper.
If you want a mental model: engines influence the shape of performance cliffs. Your harness exists to find those cliffs before your users do.
A quick note on microbenchmarks
Microbenchmarks are still useful. I treat them like unit tests for performance regressions, not like decision-makers.
TechEmpower is a good reminder that frameworks and workload mixes dominate outcomes. A runtime that wins “plaintext” can lose “JSON + DB,” and both can lose “WebSockets + fan-out.” Use TechEmpower as context, not as a verdict.
My 2026 prediction: Bun will win more greenfield services, but Node will remain the default
Bun is doing what ambitious runtimes should do. It’s compressing the stack and making “fast by default” real for a class of services.
Node is doing what mature platforms should do. It’s staying dependable while still improving.
My prediction: by the end of 2026, Bun will be a common choice for real-time, connection-heavy services where cold start and RSS are first-order costs. Node will stay the default for everything that leans on the ecosystem and long-term operability.
If you want to pick intelligently, stop arguing about ideology. Build a harness that matches your workload, publish the numbers, and let your infrastructure tell you the truth.
Internal reading trail if you’re in “production mindset” mode:
- Docker Compose vs Kubernetes for AI/ML [2026]: Use Which?
- Debug HTTP/3 QUIC in Production: 8-Step Playbook [2026]
- LLM Latency Benchmark Methodology: Streaming UX Metrics [2026]
- Reproducible Terminal Dev Environment: direnv + mise [2026]
And if you’re building systems that include AI agents in the backend, you’ll care even more about tail latency and connection stability. Start here: AI agents.
Also, if your team is trying to ship fast with Claude Code or other agentic coding tools, treat runtime changes as part of your risk surface, not just your performance plan: Claude Code.
Finally, don’t forget that “fast runtime” doesn’t save you from app-layer attacks. If you run WebSockets with user-generated content, you’re eventually going to deal with input that tries to control the system. The same mindset behind prompt injection applies to any system that executes actions based on untrusted text: prompt injection.
Originally published on kunalganglani.com
Top comments (0)