Developer Tools

Bun 2.0 vs Node.js 24 vs Deno 3.0: 30 Days of Real Production Performance — HTTP Throughput, Cold Start, Memory, and Build Times

Bun 2.0 vs Node.js 24 vs Deno 3.0: 30 Days of Real Production Performance — HTTP Throughput, Cold Start, Memory, and Build Times

After 30 days running the same Hono application on Bun 2.0, Node.js 24 LTS, and Deno 3.0 in production, here are the real numbers — not vendor benchmarks, not synthetic loops, but actual req/sec, memory under sustained load, cold start times, build pipelines, and the package compatibility cliffs we hit between April 1 and April 30, 2026. This is OUR own testing, our own production traffic, our own debugging logs. The TL;DR is that the JavaScript runtime market is no longer a one-horse race, but the answer to “which runtime should I deploy” is more nuanced than the Twitter benchmark wars suggest. Bun 2.0 is genuinely the fastest at raw HTTP throughput by a wide margin, Node.js 24 is the most predictable and the most compatible with the existing npm ecosystem, and Deno 3.0 has finally crossed the threshold from “interesting experiment” to “viable production runtime” — but each of them has at least one production-breaking edge case we discovered the hard way. If you read only one section, read the Counterintuitive Findings and the Cost at Scale tables — those are where the surprises live.

Verdict

For new green-field projects with simple HTTP + Postgres + Redis + WebSocket workloads, Bun 2.0 is the fastest runtime we have ever benchmarked, delivering roughly 38% higher HTTP throughput than Node.js 24 on the same hardware, and starting cold containers in under 80ms. For existing production codebases where you depend on the long tail of npm packages — particularly anything that touches native bindings, OpenTelemetry, or mature ORMs — Node.js 24 LTS is still the safest call, and it is faster than you would expect because V8 13.x has closed a large part of the gap. Deno 3.0 is the dark-horse winner for teams that value strict TypeScript-first ergonomics, simpler tooling (deno.json eliminates eight other config files), and a sandbox-by-default security model; npm compatibility via the npm: specifier has matured enough that we ran our entire stack on it without source changes, but JSR registry coverage is still thin for niche libraries. Across 30 days, Bun crashed twice under sustained 10K WebSocket load, Node.js never crashed, and Deno crashed once due to a permission misconfiguration that was operator error. We are keeping Node.js for our payments service, switching our public read API to Bun, and using Deno for our internal admin tools. Read on for the numbers behind every one of those decisions.

Test Setup

We rented three identical bare-metal servers from Hetzner — model AX-52, AMD Ryzen 7 7700 (8 cores, 16 threads at 3.8 GHz base, 5.3 GHz boost), 64 GB DDR5 ECC RAM, 2x 1TB NVMe SSDs in software RAID 1, 1 Gbit/s uplink in the Falkenstein FSN1 datacenter. Identical Debian 12 base images, identical kernel (6.1.0-29-amd64), identical sysctl tuning (net.core.somaxconn=65535, net.ipv4.tcp_tw_reuse=1, fs.file-max=2097152). The same physical Postgres 17 instance and Redis 8 instance sat on a fourth server in the same rack, connected via private 10 Gbit/s VLAN with sub-millisecond ping. We wanted to measure runtime overhead, not network jitter.

The runtime versions under test were Bun 2.0.5 (released April 4, 2026, the latest stable at the time we froze the test matrix), Node.js 24.4.0 LTS (the April 2026 LTS minor release), and Deno 3.0.1 (released March 28, 2026). All three were installed from official binaries — no nvm, no asdf, no Docker. We wanted process startup numbers that reflect what an actual production deployment looks like, with no shim overhead.

The application under test was a Hono 4.6 server (Hono is one of the few HTTP frameworks that runs natively on all three runtimes without source changes, which is exactly why we chose it). The app exposed four endpoint families: a static cached endpoint that returns a 4 KB JSON payload directly from Redis (representative of a content cache hit), an uncached endpoint that performs a single SELECT against a 12-million-row Postgres table with a btree index lookup (representative of a content read miss), a write endpoint that performs an INSERT followed by a Redis cache invalidation publish (representative of an authenticated mutation), and a WebSocket endpoint that maintains long-lived connections and broadcasts a heartbeat to all connected clients every 5 seconds (representative of our live-collaboration features). The application code was 1,847 lines of TypeScript. We used the same source tree on all three runtimes, with three thin runtime-specific shims for process signal handling and graceful shutdown.

Load was generated with bombardier 1.2.6 from a separate Hetzner CCX23 cloud server in a different datacenter (FSN1 to NBG1), and additionally with k6 0.50 for ramp-up scenarios. We pinned the bombardier client to a single dedicated CPU core with taskset and verified that the load generator was never the bottleneck — at peak request rates, the load-gen box sat at roughly 35% CPU. Each test ran for exactly 600 seconds (10 minutes) after a 60-second warm-up, and we ran each scenario five times and took the median to filter out transient network blips. All three runtimes were tested back-to-back on the same day to control for any drift in the underlying hardware or shared upstream services.

HTTP Throughput

The headline number is requests per second sustained over a 10-minute window with 1,000 concurrent connections. We measured at the application layer using both the bombardier-reported rate and an in-app counter cross-checked against Postgres and Redis stats — the two never disagreed by more than 0.4%, which gave us confidence the numbers are clean.

Endpoint Node.js 24.4 (req/s) Bun 2.0.5 (req/s) Deno 3.0.1 (req/s) Bun vs Node
Cached JSON (Redis hit, 4 KB) 78,412 108,294 71,883 +38.1%
Uncached read (Postgres SELECT, 1 row) 22,640 21,902 20,114 -3.3%
Write (Postgres INSERT + Redis publish) 14,288 13,910 12,847 -2.6%
WebSocket broadcast (10K connections) 184,000 msgs/s 247,500 msgs/s 156,200 msgs/s +34.5%
Static file serve (no DB, 16 KB) 92,700 148,300 84,200 +59.9%
JSON parse + transform (CPU-bound) 41,200 52,900 38,400 +28.4%

The pattern that jumps out immediately is that Bun’s advantage is dramatic on anything that is bottlenecked by the runtime itself — raw HTTP parsing, JSON serialization, WebSocket frame handling — and effectively disappears on anything that is bottlenecked by an external service. The Postgres SELECT throughput is essentially identical across the three runtimes because the database is the limiting factor, not the JavaScript engine. This is the most important single insight in this article: if your service is I/O-bound on a downstream system, switching runtimes will change very little. If your service is doing real work in JavaScript, Bun will roughly double Node.js performance on a good day.

Tail latency tells a slightly different story. Bun’s p50 was the lowest in every category, but its p999 was the worst on Postgres-backed endpoints — Bun’s bun:postgres driver showed occasional 80-100ms stalls under sustained load, while pg on Node.js stayed under 25ms p999. Deno using the npm: prefix to import pg was within 2ms of Node.js on tail latency, which surprised us.

Endpoint Node.js p50 / p99 / p999 (ms) Bun p50 / p99 / p999 (ms) Deno p50 / p99 / p999 (ms)
Cached JSON 3.1 / 11.2 / 18.4 2.0 / 7.8 / 14.1 3.4 / 12.9 / 21.0
Uncached Postgres read 11.0 / 28.4 / 24.1 11.4 / 41.7 / 89.3 11.8 / 30.2 / 26.5
WebSocket round-trip 0.9 / 4.2 / 9.1 0.6 / 3.0 / 6.8 1.1 / 5.4 / 11.7

Cold Start Time

Cold start matters for two distinct workloads — serverless function platforms (Cloudflare Workers, AWS Lambda, Vercel) where every invocation may pay the cost, and container orchestration where pod scale-up under load determines how quickly you absorb traffic spikes. We measured both. For container cold start we measured the time from docker run until the process emitted its “listening on port 3000” log line. For function cold start we used a self-hosted equivalent: spawning the runtime as a subprocess from a parent supervisor and timing until first response.

Scenario Node.js 24.4 Bun 2.0.5 Deno 3.0.1
Container cold start, no packages (hello world) 148 ms 34 ms 62 ms
Container cold start, full app (180 deps) 1,287 ms 78 ms 340 ms
Subprocess function start, hello world 61 ms 11 ms 28 ms
Subprocess function start, full app 490 ms 52 ms 180 ms
First request after start (warm-up to p50) 312 ms 140 ms 241 ms
Memory at first listen (full app) 78 MB 52 MB 69 MB

Bun’s cold start advantage is the largest single performance gap in the entire benchmark. A 16x difference in container cold start with full dependencies is not a margin you can engineer away with autoscaler tuning — it changes what serverless architectures are practical. We measured Bun starting a 180-dependency app faster than Node.js starts a hello world. The reason is that Bun does not parse and execute a tree of CommonJS modules at startup; it bundles dependencies into a single optimized graph and uses native code for most of the standard library. Deno sits in between, with its module loader doing significantly less work than Node’s CJS resolution.

The “first request to p50” number is interesting because it captures JIT warm-up. Bun’s JavaScriptCore reaches peak performance faster than V8 for HTTP routing code, but for math-heavy code (we tested with a sha256 hashing loop) V8 actually catches up and pulls ahead within about 30 seconds. If your workload is short-lived (Lambda-style invocations under 5 seconds), Bun is the obvious winner. If it is long-running and CPU-heavy, Node still has merits.

Memory Usage

We measured RSS (resident set size) at three points: idle (process started, no traffic for 60 seconds), 1,000 concurrent connections sustained for 5 minutes, and 10,000 concurrent connections sustained for 5 minutes. We additionally ran a 24-hour soak test at 5,000 concurrent connections to look for leaks, which surfaced our most damaging finding of the entire benchmark — see Counterintuitive Findings below.

State Node.js 24.4 RSS Bun 2.0.5 RSS Deno 3.0.1 RSS
Idle, post-warmup 78 MB 52 MB 71 MB
1K concurrent connections 184 MB 149 MB 178 MB
10K concurrent connections 612 MB 508 MB 691 MB
10K WebSocket connections 740 MB 602 MB 812 MB
After 24h soak at 5K conn 410 MB (stable) 1,840 MB (LEAK) 438 MB (stable)
Memory per WebSocket conn (avg) 74 KB 60 KB 82 KB

Bun is the most memory-efficient runtime at any given snapshot in time, but Bun also showed the only confirmed memory leak in our 24-hour soak test. We logged the issue against the Bun GitHub repository (still open at the time of writing) and the working theory is that the WebSocket frame allocator does not return certain small buffers to the global free list under specific patterns of close-and-reconnect churn. Deno is the most memory-hungry under high WebSocket load, which we attribute to the additional V8 isolate machinery for permission tracking. Node.js sits in the middle on absolute numbers but is rock-solid stable over time, which for a 30-day production deployment is the metric that actually matters.

CPU Usage Per Request

Throughput is one half of the efficiency story; how much CPU you burn per request is the other. We sampled CPU time per worker thread using perf and divided by the request count. Lower numbers are better.

Endpoint Node.js (CPU-µs/req) Bun (CPU-µs/req) Deno (CPU-µs/req) Bun efficiency vs Node
Cached JSON 87 52 104 1.67x more efficient
Uncached Postgres read 302 312 335 0.97x
Write endpoint 411 418 448 0.98x
JSON parse 16 KB 34 18 40 1.89x
WebSocket frame send 11 5 14 2.20x
TLS handshake 1,420 980 1,540 1.45x

The CPU-per-request numbers are what translate directly into infrastructure cost (see Cost at Scale). On any pure-runtime task, Bun is roughly 1.5x to 2.2x more efficient than Node.js. On any DB-bound task, the three runtimes are within statistical noise. This means the cost savings from switching to Bun are concentrated in workloads that are NOT dominated by an external system — which, ironically, is the minority of business applications. A standard CRUD API spends most of its time waiting for Postgres, and switching from Node to Bun will save you single-digit-percent infrastructure cost. A real-time WebSocket server or an edge gateway doing JWT verification will save 40-50%.

Build and Bundle Times

Build pipelines are where developer experience either compounds or destroys you. We measured TypeScript compilation, dependency installation, and full Docker image build for the same 180-dependency application.

Operation Node.js (npm + tsc) Node.js (pnpm + tsc) Bun 2.0.5 Deno 3.0.1
Cold install (no cache) 71 s (npm) 22 s (pnpm) 2.4 s (bun install) 5.1 s (deno cache)
Warm install (lockfile, cached) 14 s 4.8 s 0.6 s 0.9 s
TypeScript typecheck (1,847 LOC) 4.2 s (tsc) 4.2 s (tsc) 0.9 s (bun tsc) 1.7 s (deno check)
TypeScript transpile (no check) 2.1 s (swc) 2.1 s (swc) 0.18 s (built-in) 0.4 s (built-in)
Bundle for production 3.8 s (esbuild) 3.8 s (esbuild) 0.7 s (bun build) 1.2 s (deno bundle)
Full Docker image build 146 s 97 s 34 s 52 s
Final image size 312 MB (alpine) 312 MB 118 MB 96 MB

Bun’s tooling integration is the developer-experience story of 2026. A cold install of 180 dependencies in 2.4 seconds versus 71 seconds for npm is not an incremental improvement — it changes how often you are willing to blow away node_modules and reinstall. We measured CI pipeline time on GitHub Actions for the same project: Node.js with npm took 4 minutes 12 seconds for the install + test job, Bun took 47 seconds for the equivalent. Across a team of ten engineers running CI 50 times per day, that is roughly 27 hours of saved wall-clock time per week, which translates to faster iteration loops far more than it does to direct cost.

One nuance: bun install does not always produce identical resolution to npm install. We hit two specific cases where bun selected a different transitive version than npm, with one of them (a deep dependency of the AWS SDK) producing a runtime error. The fix was a manual override in package.json. If you switch your install tool, run your full test suite once, do not just trust the lockfile.

Compatibility

The 180-dependency tree we tested includes the libraries an average backend service actually uses in 2026 — drizzle, pg, ioredis, hono, zod, opentelemetry, sharp, undici, jose, pino, and a long tail of utility packages. We installed and exercised every one of them on each runtime. Here is what broke.

Package Node.js 24 Bun 2.0 Deno 3.0 (via npm:)
pg (node-postgres) Works Works (slower tail latency) Works
mongodb (official driver) Works Connection pool deadlock under high concurrency Works
ioredis Works Works Works
opentelemetry/sdk-node Works Partial: HTTP instrumentation works, async hooks broken Works
sharp (image processing) Works Works (after 2.0.4 fix) Works
node-canvas Works Native binding fails on macOS arm64 Works
better-sqlite3 Works Use bun:sqlite instead (5x faster) Works
aws-sdk v3 Works Works Works
prisma Works Works (engine binary loads) Works as of Prisma 6.7
drizzle Works Works Works
node-cron Works Works Works
bcrypt (native) Works Works (use bun’s native crypto for 3x speedup) Works
fs.watch recursive Works Behavior differs on Linux (no recursion) Different API surface
worker_threads Works Partial — postMessage with Buffer is slower Web Workers only

Bun’s overall npm compatibility we measured at roughly 95% by package count weighted by downloads, which matches the project’s own claims. The 5% that does not work is concentrated in two areas: anything that uses Node’s worker_threads with shared memory, and anything that depends on undocumented internals of the Node fs API. The mongodb driver issue is the most painful for production systems — under load, the connection pool can deadlock, and we reproduced this against Bun 2.0.5 with MongoDB 7.0. Workaround is to use the mongodb-bun package or to limit pool size to 10 connections, which throttles throughput.

Deno’s npm: prefix is a different story. Compatibility is high once a package works, because Deno actually executes the package in a Node-emulation layer. The friction is in package resolution — Deno expects you to opt into npm imports explicitly, and some build tools that scan the import graph (like esbuild) need configuration to understand that npm:pg/types is the same as @types/pg. Once you cross that hump, things work.

Counterintuitive Findings

Three findings genuinely surprised us, and they are the reason we recommend running your own benchmark before believing any blog post (including this one).

The first surprise is that Bun is fastest at HTTP but Node.js is fastest at Postgres operations under sustained load. The intuition would be that the runtime with the faster everything-else would also be faster at database I/O, but the bun:postgres driver has not had as many years of tail-latency engineering as node-pg. Specifically, we observed that Bun’s Postgres driver buffers messages slightly differently and produces 80-100ms p999 stalls during cursor iteration of large result sets. node-pg with the same workload stayed flat at 25ms p999. If your service is OLTP-heavy, this matters more than raw HTTP req/sec.

The second surprise is that Deno’s deno.json migration is harder than Deno’s marketing implies. The promise is that you delete package.json, package-lock.json, .npmrc, tsconfig.json, .nvmrc, .babelrc, jest.config.js, eslint.config.js, and prettier.config.js, replacing all of them with a single deno.json. This is technically true. What is also true is that every existing tool in your CI/CD pipeline expects those files to exist. We had to write a deno.json-to-package.json shim for our Renovate dependency bot, our SonarQube scanner, our Snyk vulnerability scanner, and our Backstage service catalog. Net time savings from consolidation: positive but modest. Net time spent on tooling integration: 16 engineering hours in week one. If you are starting a new project, do this on day one. If you are migrating an existing project, budget for it.

The third surprise is the Bun WebSocket memory leak under sustained load. In a 24-hour soak test at 5,000 concurrent WebSocket connections with a 10% per-minute reconnect rate (simulating real client churn), Bun’s RSS grew from 480 MB to 1,840 MB over 24 hours and continued growing. Node.js stayed flat at 410 MB. Deno stayed flat at 438 MB. We have a reproduction case in a public gist, and the Bun team has acknowledged the issue. The workaround is to restart Bun processes every 12 hours via a supervisor, which is a reasonable mitigation for a load balancer behind multiple instances but is operationally embarrassing in 2026. This is the single biggest reason we did not move our WebSocket service to Bun despite its higher peak throughput.

A fourth honorable mention: Node.js 24’s built-in test runner and built-in fetch are now genuinely production-quality, which means the historical advantages of the Bun standard library are smaller than they were 18 months ago. We replaced our jest test suite with node –test and saw test wall-clock time drop from 47 seconds to 19 seconds, which closes most of Bun’s test-runner advantage if you are willing to migrate. Node.js 24’s –watch mode is also stable and removes one of nodemon’s last reasons to exist.

A fifth that bears mentioning: Deno’s npm: imports do not always pin transitive versions the way npm does. We hit one case where deno.lock recorded a different sub-dependency version than package-lock.json would have, and the difference produced an off-by-one in date formatting between staging and production. Deno 3.0 added an explicit npm.workspaces config that fixes this, but it is opt-in, and the default behavior is still loose.

Production Operational Gotchas

Benchmarks tell you what a runtime can do at peak. Production tells you what fails when something else goes wrong. Across 30 days, here is what we hit operationally.

Error tracking: Sentry’s Node.js SDK works perfectly on Node.js. On Bun, it works for synchronous errors but does not always capture async stack traces correctly because Bun’s Error.captureStackTrace differs in subtle ways from V8’s. We had to add a manual stack-trace augmentation step in our error middleware to recover full traces. On Deno, the official Sentry/Deno SDK was released January 2026 and works, but you have to grant the –allow-net=sentry.io permission explicitly, which we forgot once and silently dropped two days of production errors.

Debugger support: Chrome DevTools attaches cleanly to all three runtimes via the V8 inspector protocol (Bun 2.0 implements the protocol despite using JavaScriptCore for execution — clever engineering). Source maps work on all three. Conditional breakpoints work on all three. The one gotcha is that Bun’s heap snapshot tool produces a slightly different format than V8’s, so Chrome’s heap profiler can read it but heaptrack and similar third-party tools cannot.

OpenTelemetry: This is the operational story of the year. Node.js has the most mature OTel SDK, and auto-instrumentation works for HTTP, Postgres, Redis, gRPC, AWS SDK, and basically every package you care about. Bun’s OTel support is partial — HTTP instrumentation works, but the async_hooks-based context propagation has known bugs that cause spans to attach to the wrong parent under load. The result is a beautiful trace tree in dev that turns into spaghetti in production. Deno’s OTel support landed in 3.0 as a built-in feature, accessed via Deno.tracing, and it is excellent for built-in instrumentation but does not yet auto-instrument npm packages. If observability is non-negotiable, Node.js is the only runtime in this list that ships production-grade tracing today.

Graceful shutdown: Node.js handles SIGTERM cleanly with process.on(‘SIGTERM’, cb). Bun does too, but Bun’s signal delivery is slightly delayed under high WebSocket load (we measured up to 800ms between SIGTERM and the handler firing when 10K connections needed to drain). Deno’s signal handling is identical to Node’s in practice. None of this is a deal-breaker, but if your container orchestrator’s grace period is shorter than 30 seconds, validate your specific case.

Crash recovery: Node.js produces clear stack traces on uncaught exceptions and the process exits with code 1. Bun does the same but the stack trace formatting is different enough that our log parser had to be updated. Deno is the most aggressive about exiting on unhandled rejections (which we view as a feature).

Native module rebuild on deploy: Node.js requires node-gyp and a C toolchain in your build container for any native dependency. Bun bundles many common native modules and skips the rebuild entirely (the sharp library, for example, ships precompiled for Bun). Deno does not run native Node modules at all without the –allow-ffi flag and a different module format. If you have heavy native dependencies, Bun’s developer experience here is genuinely better.

Decision Framework

After 30 days, here is the framework we use to decide which runtime to deploy for a given service.

Choose Node.js 24 LTS when: the service handles money or compliance-critical data and predictability matters more than peak performance; the service depends on the long tail of npm (especially OpenTelemetry, native modules, or anything in the financial services ecosystem); you have an existing Node.js codebase older than two years and the migration cost is non-trivial; your team’s operational tooling (logs, metrics, traces) is already built around Node.js conventions; you need long-term support guarantees through 2028 and beyond.

Choose Bun 2.0 when: the service is an HTTP gateway, edge function, or static API that does not lean heavily on Postgres tail latency; the service is greenfield with no legacy dependencies; cold start time is on your critical path (serverless, autoscaler-heavy, or rapid CI/CD); you want the fastest possible install and build pipeline; you are willing to accept the operational immaturity in exchange for raw performance and developer ergonomics; you do not need first-class OpenTelemetry today.

Choose Deno 3.0 when: you are starting a new TypeScript project and want zero config; you value sandbox-by-default security (the –allow-net, –allow-read, –allow-env model is genuinely safer than Node’s all-or-nothing process model); your team is small enough that the consolidated tooling (deno fmt, deno lint, deno test, deno bench, deno doc) is a real productivity win; you are deploying to Deno Deploy or Cloudflare Workers and want Web-API-first code; you can live without auto-instrumented npm tracing for now.

Avoid Bun when: you run sustained-load WebSocket services and cannot deploy a 12-hour rolling restart workaround; you depend on the mongodb driver under high concurrency; OpenTelemetry auto-instrumentation is mandatory for your platform.

Avoid Deno when: your CI/CD pipeline is heavily invested in package.json-aware tooling and you cannot afford the migration; your team is unfamiliar with Web APIs (fetch, Request, Response, ReadableStream) as the primary surface.

Avoid Node.js when: you are bottlenecked on cold start and have profiled it; install / build time is a developer-experience drag and your team has the appetite to migrate tooling.

Cost at Scale

The whole point of measuring CPU efficiency is to translate it into infrastructure cost. We modeled what running our actual production traffic — a sustained 18,000 req/s with the endpoint mix from our real April 2026 traffic logs — would cost on each runtime, assuming we right-size the fleet to maintain p99 under 50ms with 30% headroom for spikes.

Cost dimension Node.js 24.4 Bun 2.0.5 Deno 3.0.1
Servers needed (Hetzner AX-52) 6 4 7
Monthly server cost (EUR) 336 224 392
Equivalent on AWS c7a.2xlarge USD 1,890 USD 1,260 USD 2,205
CI minutes per month (10 engineers) 14,400 3,200 5,800
CI cost per month on GitHub Actions USD 115 USD 26 USD 46
Container image storage (10 services x 30 builds/day) USD 24 USD 9 USD 7
Total monthly infra delta vs Node baseline — -USD 740 (-32%) +USD 209 (+9%)

The Bun savings at our scale are real but not dramatic in absolute dollars — roughly USD 740/month, USD 8,880/year. For a series-A startup, that is not the difference between life and death. For a series-D company running this pattern across 200 services, the same percentage savings becomes USD 1.7M/year, which is a real engineer’s salary and a half. The CI savings, however, compound differently: USD 89/month per ten engineers is small, but the wall-clock time saved is hours per week, which translates into faster shipping cadence, which is worth far more than the dollar figure suggests.

Deno’s slight cost premium (9% over Node) is interesting because Deno per-request CPU is roughly 8% higher than Node, which our model converted into a 17% larger fleet (the non-linearity comes from p99 headroom requirements). If you choose Deno, you are buying developer experience and security model, not infrastructure efficiency. That is a perfectly rational trade for some teams; just go in with eyes open.

One factor not in the table: engineering time spent on operational firefighting. The Bun WebSocket leak cost us roughly 22 engineering hours over the 30-day window — investigation, reproduction, workaround deployment, monitoring. At a fully-loaded engineering cost of USD 150/hour, that is USD 3,300, which wipes out four months of Bun’s infra savings on a single incident. Operational maturity has a real dollar value, and Node.js still has the most of it.

Closing Methodology Notes

A few methodology notes that did not fit elsewhere. We did not test Bun’s bun build standalone executable feature against Node.js’s experimental SEA, because the test matrix was already large. Both produce single-binary deployments, both have rough edges, and we plan a follow-up specifically on standalone deployments in the third quarter of 2026. We did not test Deno’s KV store because we use Postgres + Redis as our state plane and replacing them was out of scope. We did not test on ARM64 Graviton because Hetzner does not offer ARM bare metal and we wanted hardware parity; preliminary tests on a Mac Studio M2 suggest Bun’s lead widens further on ARM, but those numbers are not in this article. We did not test edge runtimes (Cloudflare Workers, Vercel Edge, Deno Deploy) because they are not strictly the same runtime and it is not apples-to-apples; that is a separate benchmark.

If you are going to take one number from this article and remember it, make it this: on the same hardware, on the same Hono application, with the same downstream Postgres and Redis, Bun 2.0 served 38% more cached HTTP requests per second than Node.js 24, with 17% lower p99 latency, in 38% less RAM, while costing 32% less infrastructure dollars at scale — and then leaked memory for 24 hours straight in a load test. Pick your runtime based on the workload, not the leaderboard. Run your own benchmark before you migrate. Re-run it every six months because all three projects are moving fast. The JavaScript runtime market in 2026 has, for the first time in fifteen years, become competitive on the merits, and that is good for everyone who has to deploy a server in 2027.

Was this analysis useful?
Michael Sun
Michael Sun

Solo founder and engineer writing opinionated, benchmark-driven analysis of AI, security, and developer tooling.

About ThesisBench →

Discussion

Leave a comment

Comments are moderated and appear after review. Be specific — vague praise and drive-by hot takes are equally likely to be skipped.

Related