Short version for the impatient: yes, I run Bun in production. No, it didn’t replace every Node service I have. The thing that actually changed my mind wasn’t raw runtime speed. That’s been oversold for two years. It was bun install.
I tried Bun the first time back in 0.6 days and bailed within an afternoon. Three native modules wouldn’t compile, my test runner exploded on a TypeScript decorator, and the cherry on top was a segfault in production-mode bun build that I couldn’t reproduce locally. I quietly closed the tab and went back to pnpm with tsx and Vitest, and I assumed that would be the story for a while.
Then 1.2 shipped. I gave it another weekend. A year later, I run two services and most of my CI on Bun, and I have specific reasons for the Node services I didn’t migrate. This post is that audit, with the code and the regressions, not the marketing.
Why I gave Bun another shot
The thing that pulled me back wasn’t a benchmark. It was a Stripe webhook service I was rewriting that needed to do roughly three things: verify a signature, write a row to Postgres, fire a Slack notification. The whole service was about 200 lines of TypeScript. The Node version of it had grown a tsconfig, a Jest config with a ts-jest transformer, a Dockerfile that needed node-gyp for one transitive dep, and a CI job that took 90 seconds to install dependencies before it could run a 4-second test suite.
That ratio felt absurd. So when Bun 1.2 dropped with stable Node compat for the things I actually used, I rebuilt the same service in Bun and measured.
CI went from 94 seconds to 11. The test runner ran without configuration. The Dockerfile shrank to seven lines. I shipped it on a Friday and watched the metrics over a weekend. Nothing exploded.
That’s when I started looking at the rest of my Node services and asking which ones would actually benefit and which wouldn’t.
bun install changed the shape of my CI
This is the underrated part. Everyone talks about runtime perf, but for me the biggest day-one win is the package manager. On a medium TypeScript repo (around 700 deps in the resolved graph), here’s what I measured last week on a cold runner:
npm install → 51s
pnpm install → 22s
bun install → 4s
I’ve ranted about package manager speed before in my pnpm vs npm post, and pnpm was already a big jump from npm. Bun is just another step. The lockfile is a binary format that resolves faster, and the installer hits the filesystem in a way that pnpm’s content-addressed store doesn’t quite match for cold installs.
You can keep your existing package.json. Bun reads it. The first time you run bun install, it generates a bun.lock next to your package-lock.json. I left both checked in for a few weeks and confirmed they stayed in sync before deleting the npm lockfile.
The interesting wrinkle: I use bun install in CI for every project now, even the ones still running on Node at runtime. You don’t have to commit to Bun as a runtime to get the install speedup. That alone was worth the migration in a couple of repos where the install step was the longest part of CI.
bun test made me delete Jest
The other thing I didn’t expect was the test runner. Jest with TypeScript has been a slow argument I’ve been losing for years: ts-jest vs @swc/jest, ESM compatibility, mock hoisting, transformIgnorePatterns. The Vitest migration helped but didn’t eliminate the config sprawl.
Bun ships a test runner that reads Jest-style globals (describe, it, expect) and runs .ts files directly. The migration in one of my repos was: delete jest.config.js, delete jest-environment-jsdom, delete @types/jest, delete the ts-jest chain, change one CI line from pnpm test to bun test.
Before:
// jest.config.js — 38 lines I haven't read in two years
module.exports = {
preset: 'ts-jest/presets/default-esm',
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts'],
transform: {
'^.+\\.tsx?$': ['ts-jest', { useESM: true, isolatedModules: true }]
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1'
},
// ...etc
};
After:
// nothing. bun test just runs.
On the same test suite, Jest with ts-jest took 6.4s. Bun test took 0.9s. I want to be careful here: that ratio shrinks on larger suites where the test work dominates over startup. But on a small backend service it removes a meaningful chunk of inner-loop friction.
Vitest is still my pick when I need the snapshot UI, jsdom-style component testing, or watch mode in a complex monorepo. For backend services, Bun’s test runner has eaten most of my Jest config.
bun.serve replaced my Express skeleton
The other place Bun won is server boilerplate. I had a tiny Express skeleton I copy-pasted into every new service: a logger middleware, a JSON body parser, a couple of routes, a health check. It always ended up being 60-ish lines.
Here’s the same shape in Bun.serve using the 1.2 routes API:
Bun.serve({
port: 3000,
routes: {
"/health": new Response("ok"),
"/webhook": {
POST: async (req) => {
const body = await req.json();
await processWebhook(body);
return Response.json({ received: true });
},
},
},
});
That’s it. There’s no body parser package to install. JSON handling is built in. The routes API in 1.2 is closer to what I want than Express middleware ever was. Websockets are handled by the same primitive with a separate websocket config. I haven’t had a websocket workload that pushed it yet, but the basic chat demo I wrote held up fine under a few thousand concurrent clients on a single core.
If you want middleware patterns and a broader plugin ecosystem, Hono runs natively on Bun and gives you the Express-ish ergonomics without dragging in express itself. That’s what I reach for now when a service has more than four routes.
Where Node still won
I want to be specific here, because the honest version of this post is about the things that didn’t work.
I tried to move an image processing service that uses sharp. The native module worked under Bun, but the cold-start time on AWS Lambda Node runtime vs running Bun inside a container was worse for my workload. I switched the same code back to Node 22 on Lambda’s official runtime and saved about 200ms of cold start. Not Bun’s fault. Lambda doesn’t ship Bun as a managed runtime, and the container layer overhead is real for spiky traffic.
I also kept a service on Node because I rely on Datadog’s APM auto-instrumentation. Datadog’s tracer hooks into Node’s async_hooks in a specific way. Bun has its own implementation of node:async_hooks, but I had two traces showing up wrong when I tested last quarter, and I didn’t have budget to debug Datadog vs Bun in production. It probably works now. I haven’t re-tested. If you depend on heavy APM tracing, run a real check on your own workload before committing.
The other thing that bit me: a small dependency used process.binding, a deprecated Node internal that Bun doesn’t expose. The package hadn’t been updated in three years. I forked it, removed the call, and submitted a PR. The PR has been open since March.
The pattern: when something breaks on Bun in 2026, it’s almost always a dependency reaching into a Node internal that nobody should be reaching into, or a native module that Lambda’s runtime is better positioned for. The list of broken things shrinks every month. Bun keeps a Node API compatibility page that’s actually accurate now, which was not true a year ago.
How I decide today
I don’t run Bun for everything. I run it where the tradeoffs lean in.
A new internal HTTP service that does I/O and talks to Postgres or Redis? Bun. The startup speed, the install speed, the test runner, and the lack of config tax all compound. It’s just less friction in the loop where I actually spend time.
Anything deployed on AWS Lambda where the managed Node runtime gives me cold-start advantages? Node 22. The official runtime ships with modern V8 and ESM support, and the cold-start cost of bundling Bun into a container layer isn’t worth the migration for most of my Lambda surface.
A monolith with two years of tsconfig, custom Jest setup, and a deep dep tree? I leave it alone and only swap bun install into CI. Lowest-risk, highest-leverage move and it doesn’t commit me to anything at runtime.
The thing I’d push back on: do not migrate a stable, working Node service to Bun for runtime perf alone. The risk-adjusted payoff isn’t there. Migrate when you’re rewriting anyway, when CI is genuinely painful, or when your local test loop is slowing you down. That kind of iteration-speed win is the whole game in the small services I cover across my portfolio projects.
One thing to try this week
Pick the noisiest CI job in your monorepo, the one where most of the runtime is pnpm install or npm ci, and add a parallel job that runs bun install instead. Don’t change anything else. Just measure.
If bun install finishes in a tenth of the time, you’ve found the cheapest migration in your stack. Swap that one line and ship it. You don’t have to touch the runtime, the test runner, or the rest of your tooling to get the win.
That’s how I started. A year later I have a much clearer answer to the question “should I switch to Bun?” than I did before: only where the tradeoffs lean in. Most of the time, in the kind of services I build, they do.