Every few months, the JavaScript ecosystem gets a new "Node.js killer" and developers collectively lose their minds on Twitter. Deno had its moment. Now it's Bun. The benchmarks look insane — HTTP throughput numbers that make Node.js look like it's running on a Raspberry Pi from 2014. So we spent a few weeks actually testing it on real projects instead of hello-world servers, and here's the honest breakdown.
What Bun Actually Is (Beyond the Marketing)
Bun is a JavaScript runtime built on JavaScriptCore (WebKit's engine, the same one Safari uses) instead of V8. It's written in Zig, which is a lower-level systems language. The whole thing was built from scratch to be fast — not "we optimized a few hot paths" fast, but "we rethought everything" fast.
But here's the thing most blog posts gloss over: Bun isn't just a runtime. It's also a package manager, a bundler, a test runner, and a transpiler. It's trying to replace npm, webpack, Jest, and Node.js in one binary. That's either brilliant or terrifying depending on your tolerance for putting all your eggs in one basket.
- Runtime: Replaces Node.js for executing JavaScript/TypeScript
- Package manager: Replaces npm/yarn/pnpm — installs are genuinely 10-20x faster
- Bundler: Replaces esbuild/webpack for building your app
- Test runner: Replaces Jest/Vitest with a mostly-compatible API
- Transpiler: Runs TypeScript and JSX natively without extra config
The last one is legitimately great. No tsconfig dance, no ts-node, no build step for scripts. You just run `bun script.ts` and it works. For quick tooling scripts and one-off utilities, this alone might be worth it.
The Benchmarks: Real or Marketing?
Bun's own benchmarks show it handling 3-5x more HTTP requests per second than Node.js. And when you run them yourself, yeah — those numbers hold up. For raw I/O bound workloads, Bun is genuinely faster.
But here's what the benchmarks are measuring: hello-world HTTP servers. `return new Response('Hello World')` in a tight loop. In production, your app is doing database queries, calling third-party APIs, running business logic, and serializing complex objects. The bottleneck is almost never the runtime's HTTP throughput. It's your Postgres query that doesn't have an index, or the Stripe API taking 800ms to respond.
// Bun's HTTP server — this IS faster than Node's http module
const server = Bun.serve({
port: 3000,
fetch(request) {
return new Response('Hello World');
},
});
console.log(`Listening on http://localhost:${server.port}`);
// But in reality, your handler looks more like this:
async function realWorldHandler(request: Request) {
const userId = getUserFromJWT(request.headers.get('Authorization'));
// This DB call takes 20-200ms — runtime overhead is noise
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
with: { subscription: true, projects: true },
});
// Third-party API call — another 100-500ms
const stripeCustomer = await stripe.customers.retrieve(user.stripeId);
return Response.json({ user, stripeCustomer });
}In that second scenario, whether Bun or Node.js handles the request in 0.1ms or 0.3ms is completely irrelevant. You're waiting on I/O, not the runtime.
Where Bun Actually Wins
Package installation speed is not a lie. We tested on a mid-size Next.js project with about 180 dependencies. Cold install with npm: 47 seconds. With Bun: 6 seconds. That's real, it's consistent, and it compounds — CI pipelines get significantly faster, local `npm install` after pulling branches becomes instant.
# Switch to Bun just for package management (no runtime change needed)
# Remove your existing lockfile first
rm package-lock.json # or yarn.lock, pnpm-lock.yaml
# Install bun
curl -fsSL https://bun.sh/install | bash
# Use bun for installs, keep running your app with Node
bun install
# bun.lockb is created — commit this to git
# Your package.json doesn't change at all
# In CI (GitHub Actions example):
# - uses: oven-sh/setup-bun@v1
# with:
# bun-version: latest
# - run: bun install --frozen-lockfileTypeScript support out of the box is the other genuine win. If you're running scripts, CLIs, or small services, not having to set up a TypeScript compilation step is a real quality-of-life improvement.
The test runner is also worth a look. If you're starting a new project and don't have existing Jest config, `bun test` is fast and the API is familiar. We're talking 2-3x faster test suite execution on a medium-sized test suite. That's not noise.
The Compatibility Problem Nobody Talks About Enough
Bun claims Node.js compatibility, and for most things it's true. But "most things" is doing a lot of work in that sentence.
We hit issues with native addons — packages that use compiled C++ bindings. These are things like `sharp` (image processing), some database drivers, and certain cryptography libraries. Bun has its own implementations of some Node APIs but they're not always drop-in compatible. You might not hit any of these, or you might spend three hours debugging why your image resizing pipeline silently fails.
// Things that work great in Bun:
import { readFileSync } from 'fs';
import { createServer } from 'http'; // Works but use Bun.serve() instead
import bcrypt from 'bcryptjs'; // Pure JS version, fine
import { drizzle } from 'drizzle-orm/bun-sqlite'; // Bun-specific adapter
// Things that might give you grief:
// - sharp (native addon for image processing)
// - node-canvas
// - Some versions of better-sqlite3
// - Anything using worker_threads in complex ways
// - Some Webpack plugins in build tools
// Check compatibility before committing:
// https://bun.sh/guides/ecosystem
// If you're using Next.js, just check if your version supports Bun:
// Next.js 14.1+ has experimental Bun support for dev server
// Production builds still go through Node (or Vercel's runtime)Before migrating anything production-critical to Bun as a runtime, audit your dependencies for native addons. Run `npm ls --depth=0` and check each one's Bun compatibility. The package manager switch is almost always safe. The runtime switch needs more diligence.
Next.js + Bun: The Specific Case
Since most of you are probably here because you're running Next.js, here's the specific situation: Next.js has experimental Bun support for the development server as of 14.1. Vercel (where most people deploy Next.js) runs your app on their own managed runtime — switching to Bun locally doesn't change what runs in production.
The dev server startup with Bun is noticeably faster. `bun --bun next dev` versus `next dev` — you'll feel the difference on larger projects. But your production build behavior on Vercel is unchanged. If you're self-hosting with Docker, you can run your Next.js app on Bun, but you need to test it carefully because the App Router does some things that Bun's Node compatibility layer still occasionally chokes on.
# For Next.js projects — lowest risk way to try Bun
# 1. Switch package manager only (safest, immediate wins)
bun install
# 2. Use Bun for scripts in package.json
# package.json:
# "scripts": {
# "dev": "next dev", <-- Bun runs this but Node executes Next
# "build": "next build",
# "db:migrate": "bun run drizzle-kit migrate" <-- Pure TS scripts run great
# }
# 3. Experimental: Use Bun as the runtime for Next.js dev
bun --bun next dev
# (The --bun flag tells it to use Bun's runtime instead of Node)
# 4. For self-hosted production (test thoroughly first)
# Dockerfile:
# FROM oven/bun:1 AS base
# WORKDIR /app
# COPY package.json bun.lockb ./
# RUN bun install --frozen-lockfile
# COPY . .
# RUN bun run build
# CMD ["bun", "run", "start"]Our Actual Recommendation
Here's where we land after actually using this in anger: switch to Bun as your package manager today. The risk is near-zero (worst case you delete bun.lockb and go back to npm), and the install speed improvement is real and affects your daily workflow and CI bills.
Use Bun as a test runner on new projects where you don't have existing Jest configuration. The speed difference in test suites is meaningful and the API is close enough that switching isn't painful.
Use Bun as the runtime for TypeScript scripts and tooling — database seed scripts, migration runners, code generators, one-off data transformation scripts. Running `bun seed.ts` without a build step is genuinely nice.
Be more cautious about switching your production server runtime. If you're running something without complex native dependencies and you have good test coverage, go for it. If you're running a Next.js app on Vercel, it literally doesn't matter — Vercel controls the runtime. If you're self-hosting and every millisecond matters, benchmark your actual workload, not the Bun homepage numbers.
- Package manager: Switch now, zero risk, meaningful speed gains
- Test runner: Switch on new projects, migrate old ones when you have time
- TypeScript scripts: Use Bun, it's strictly better for this
- Development server: Try it, probably fine, easy to revert
- Production runtime (self-hosted): Test carefully, audit native deps first
- Production runtime (Vercel/managed): Largely irrelevant, platform controls this
The peal.dev templates we ship use `bun install` by default in the setup instructions now — purely for the developer experience improvement. The actual Next.js runtime in production is whatever Vercel runs, which is their problem to optimize.
The Real Question: Is Node.js Slow?
Node.js is not your bottleneck. We need to say this clearly because the Bun marketing (and the people enthusiastically sharing benchmarks) creates an impression that your app is slow because of Node.js. It almost certainly isn't. Your app is slow because of unindexed database queries, N+1 query problems, synchronous code blocking the event loop, or third-party APIs being third-party APIs.
We've seen apps go from 2-second response times to 80ms by adding one database index. We've never seen a runtime switch produce that kind of result. Fix your queries before you change your runtime.
Bun is a genuine improvement in developer experience, especially for install times and TypeScript support. It's not a magic performance fix for real-world applications. Use it where it helps, keep Node.js where it's working fine.
The ecosystem is moving toward Bun. Compatibility keeps improving, major frameworks are adding explicit support, and the tooling story gets better every month. If you start a new project today, using Bun for package management and scripts is the right call. For the runtime itself — especially in production — let the ecosystem mature another six months before betting your production uptime on it. Node.js isn't going anywhere, and there's no prize for being an early adopter when things go wrong at 2am.
