DEV Community

Moon Robert
Moon Robert

Posted on • Originally published at blog.rebalai.com

Bun vs Node.js in Production: What Three Months of Real Traffic Taught Me

The benchmarks are real. I just didn't expect them to matter less than everything else.

My team — four engineers, one of whom is constitutionally allergic to anything not boring and battle-tested — runs a mid-sized API service that handles about 12,000 requests per minute at peak. We're on AWS, using Hono as our HTTP framework, PostgreSQL behind a PgBouncer connection pool, and a cluster of background workers that process job queues. Nothing exotic. Nothing that would make a Hacker News thread interesting for the right reasons.

I started the Bun migration in November 2025. We went fully live in early February 2026. Bun 1.2.4 to be specific, running on Node.js 22.14 before the switch. This is what actually happened.

My Setup and Why I Actually Pulled the Trigger on This

So the honest reason I wanted to migrate wasn't startup time or raw throughput — it was Lambda cold starts. We have a handful of event-driven functions sitting alongside the main API, and on Node.js 22 we were seeing cold start times between 800ms and 1.4 seconds depending on the function's dependency footprint. That's tolerable until your product team decides to add a user-facing webhook handler that runs on Lambda, and suddenly 1.1 seconds of cold start is a conversation at standup.

I'd been watching Bun's progress since 1.0 dropped in September 2023. By mid-2025 the complaints about Node.js compatibility had quieted down considerably, most of the critical GitHub issues were closed, and the ecosystem was just... calmer about it. That felt like the right signal. I'm not an early adopter by nature — I let other people find the load-bearing bugs.

The other thing, and I should be upfront about this, was curiosity. Eight years of Node.js means I can set it up in my sleep, which is comfortable and also a little deadening. Bun forced me to actually think about the runtime again.

My skeptical teammate Marcus's concern was simple: "We have zero on-call bandwidth for runtime-level weirdness." He wasn't wrong. I promised him a staged rollout with easy rollback, which is the only reason he agreed.

The Migration Itself: Mostly Fine, Then Not Fine at All

I expected package.json compatibility to be the main friction. It wasn't. bun install just worked, bun run picked up our existing scripts, and Hono didn't need any changes at all. The TypeScript handling through Bun's built-in transpiler was a relief — no more ts-node or tsx config to babysit.

// Before: package.json scripts with ts-node
"start": "ts-node -r tsconfig-paths/register src/index.ts",
"dev": "nodemon --exec ts-node src/index.ts",

// After: clean
"start": "bun src/index.ts",
"dev": "bun --watch src/index.ts",
Enter fullscreen mode Exit fullscreen mode

That part took about a day. The test suite ran on Bun's test runner without modification, which genuinely surprised me — I expected at least one Jest quirk to bite us.

What actually cost us time was our APM setup. We use Datadog, and the dd-trace Node.js agent has its own ideas about how to hook into the runtime. Bun 1.2 has solid node: compatibility, but there are still specific internals that APM agents depend on — vm module behavior, certain async_hooks edge cases — and dd-trace was partially blind in Bun for about two weeks while I worked through the configuration. The Datadog Bun support page existed but was, charitably, aspirational. I ended up running their agent in compatibility mode and losing some auto-instrumentation for that period.

I pushed our first production deployment on a Friday afternoon and promptly discovered that our database query spans weren't showing up in Datadog. Not a crash, not a data issue — just invisible infrastructure. Which is a specific kind of Friday stress. We rolled back the APM config (not the Bun deployment, crucially) and spent the following week sorting it out properly.

By January we had full observability restored. But that two-week gap where I couldn't fully trust my traces was uncomfortable in a way that benchmark numbers don't capture.

Benchmark Numbers from 3 Months of Real Traffic

Right, so — the actual numbers. I ran synthetic benchmarks before migrating, and I'll share them, but I find the production data more interesting.

Synthetic load test, same Hono app, autocannon at 1000 concurrent connections, PostgreSQL queries included:

Runtime Req/sec P99 latency P50 latency
Node.js 22.14 9,840 48ms 11ms
Bun 1.2.4 14,120 31ms 7ms

That's a 43% throughput improvement. P99 latency dropping from 48ms to 31ms is meaningful. I wasn't expecting numbers quite that dramatic — I'd mentally budgeted for 20-25% and was prepared to explain why that wasn't enough to justify the migration risk.

Lambda cold starts, which were the original motivation: Node.js 22 averaged 940ms cold start for our heaviest function. Bun: 290ms. That one's hard to argue with.

Production traffic tells a slightly more nuanced story. Our actual P99 at production load (12k RPM peak) went from around 67ms to 44ms. The throughput headroom we gained let us downsize one EC2 instance in our API cluster — that's roughly $180/month back, which pays for roughly zero of the engineering time I spent on this but makes the conversation with my manager easier.

One thing I noticed: Bun's memory usage is lower at idle but the difference shrinks under sustained load. At peak, both runtimes were within about 15% of each other on RSS. So if your argument for Bun is memory efficiency, validate that claim under your actual traffic pattern, not synthetic benchmarks.

The Parts That Still Bit Me

The embarrassing part came when I was benchmarking our worker processes — the ones that drain job queues from Redis. I expected them to be faster in Bun, same as the API. They weren't. Essentially identical to Node.js, within noise. I spent way too long staring at this before realizing the bottleneck was always Redis round-trip latency and serialization, not the runtime itself. Bun can't make your network faster. I thought I understood that, and apparently I needed to re-learn it.

The test runner maturity is better than it was in 2024 but I'd still call it 90% there. We hit one issue with module mocking — specifically, trying to mock a module that re-exports from another file — that produced behavior inconsistent with Jest/Vitest. I filed an issue (github.com/oven-sh/bun issue #9847, for anyone tracking this), and the Bun team responded within a day. We worked around it rather than waiting for a fix.

Native addons are still occasionally a problem. We don't use many, but one internal tool in our monorepo uses sharp for image processing. sharp works in Bun, but only through a compatibility shim that the sharp maintainers added specifically for Bun support, and it's not quite as fast as native Node.js. For us that's fine — that tool isn't on a hot path. But if your app leans on native addons, audit them before committing to this migration.

The Windows story is improved but I still wouldn't use Bun on Windows for anything serious. Most of our team develops on Macs, one on Linux, so this wasn't a blocker. Your mileage may vary.

None of these were showstoppers. But they're the kind of friction that a team without the bandwidth to absorb them would find genuinely frustrating. The total debugging time I spent on Bun-specific issues over three months was probably 20-25 hours. For a migration that saved us $180/month, that math only works if you value the latency and cold-start improvements — which we do.

// One pattern I actually liked: Bun's built-in SQLite for dev fixtures
// No extra dependencies, just works
import { Database } from "bun:sqlite";

const db = new Database(":memory:");
db.run(`CREATE TABLE fixtures (id INTEGER PRIMARY KEY, data TEXT)`);

// We use this in tests now instead of spinning up a test container
// Setup time went from ~8s to ~200ms
const stmt = db.prepare("INSERT INTO fixtures VALUES (?, ?)");
stmt.run(1, JSON.stringify({ test: true }));
Enter fullscreen mode Exit fullscreen mode

bun:sqlite in tests quietly improved our feedback loop. It's not a replacement for a real container when you need actual PostgreSQL behavior, but for unit tests that just need structured data, it's been useful enough that I've stopped reaching for anything else.

After Three Months: Here's What I'd Tell You

Small team, JavaScript/TypeScript HTTP API, no heavy native addon dependencies — migrate. The performance gains are real, and compatibility has reached a point where most rough edges are papercuts rather than blockers. The cold-start improvements alone make it worth it for Lambda-heavy architectures.

If your observability stack depends on Node.js-specific APM agent internals, wait six months and re-evaluate. Datadog and New Relic are both working on better Bun support, and by mid-2026 this will probably be a non-issue. Same story if you have Windows developers who need a smooth local experience, or if your team is already stretched — there will be at least one confusing afternoon, and you need the slack to absorb it.

Node.js 22 is a genuinely good runtime — fast, stable, with a decade of ecosystem hardening behind it. I'm not arguing against it. If I were starting a new project today with no strong opinion either way, I'd use Bun. If I were maintaining a large existing codebase with a team that has zero interest in debugging runtime quirks, I'd stay on Node.js for now.

We're staying on Bun. The P99 latency difference shows up in our frontend performance metrics in ways that feel good to point at. Marcus, my skeptical teammate, has stopped complaining about it — which, from him, is basically a glowing endorsement.

Top comments (0)