Bun
I kept seeing Bun in benchmarks, Twitter threads, and conference talks. The pitch was always the same: it's faster than Node.js. Faster startup, faster installs, faster everything. I was skeptical—I've been burned by "faster" tools that trade speed for compatibility—but I spent a few weeks building with it, and I came away with a more nuanced take than "it's fast."
After writing about the JavaScript runtime—the engine, the event loop, the task queues—I wanted to understand what happens when you swap out the entire foundation. Bun doesn't just tweak Node.js. It replaces the engine, rewrites the I/O layer, and bundles a package manager, bundler, and test runner into a single binary. It's an ambitious bet that the JavaScript toolchain can be radically simpler and faster. Here's what I learned.
What Bun Actually Is
Bun is four things in one binary:
- A JavaScript/TypeScript runtime — like Node.js or Deno, it executes your code.
- A package manager — like npm, yarn, or pnpm, it installs dependencies.
- A bundler — like esbuild or webpack, it bundles your code for production.
- A test runner — like Jest or Vitest, it runs your tests.
The runtime is built on JavaScriptCore (JSC), the engine that powers Safari. Node.js and Deno both use V8 (Chrome's engine). This is the most consequential architectural decision in Bun, and it has real implications.
V8 and JSC take different approaches to JIT compilation. V8 has a single optimizing compiler (TurboFan) that produces highly optimized machine code but takes longer to warm up. JSC has a multi-tier pipeline—it starts interpreting immediately, then progressively compiles through three tiers (LLInt → Baseline → DFG → FTL) as functions get hotter. The result: JSC starts executing code faster. For long-running servers, the difference narrows as V8's optimizer kicks in. For short-lived scripts and CLI tools, JSC's faster startup is noticeable.
The other key decision: Bun's native layer is written in Zig, not C++ (Node.js) or Rust (Deno). Zig gives manual memory control without a garbage collector in the runtime layer itself. JavaScript still has its GC (managed by JSC), but the I/O, HTTP parsing, and file system operations happen in Zig with predictable memory behavior. The Bun team claims this is a significant factor in their throughput numbers, and from what I've seen in the benchmarks I trust, it holds up for I/O-heavy workloads.
Getting Started
Setting up a project with Bun is noticeably frictionless. There's no separate TypeScript compilation step, no tsconfig.json required to get started, no ts-node or tsx wrapper.
# Install Bun
curl -fsSL https://bun.sh/install | bash
# Create a new project
bun init
# Run a TypeScript file directly
bun run index.ts
# Install dependencies
bun installbun init scaffolds a project with a package.json, tsconfig.json, and an entry point. bun run executes TypeScript natively—no build step, no transpiler config. This alone makes it compelling for scripts and prototyping.
Here's where the difference hits you first—a simple HTTP server:
// Bun
Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url)
if (url.pathname === '/') {
return new Response('Hello from Bun')
}
return new Response('Not found', { status: 404 })
},
})
console.log('Listening on http://localhost:3000')// Node.js
import { createServer } from 'node:http'
const server = createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('Hello from Node.js')
} else {
res.writeHead(404)
res.end('Not found')
}
})
server.listen(3000, () => {
console.log('Listening on http://localhost:3000')
})Bun's Bun.serve() uses the web-standard Request and Response objects. If you've written Cloudflare Workers or Deno code, this API is immediately familiar. Node's http.createServer uses its own IncomingMessage and ServerResponse objects—an API designed in 2009 that predates web standards.
The Bun version is fewer lines, but more importantly, it's portable. The same Request/Response pattern works in Deno, Cloudflare Workers, and Vercel Edge Functions. Code that uses Node's http module is locked to Node.
Speed: Where It's Real and Where It's Marketing
Bun's speed claims fall into three categories, and they're not equally meaningful.
Startup time — genuinely faster
Bun starts executing code significantly faster than Node.js. For a "hello world" script, Bun is roughly 4-5x faster to start. This matters for CLI tools, serverless cold starts, and scripts that run frequently. If you're running a linter, a code generator, or a dev server that restarts on file changes, faster startup compounds.
// benchmark-startup.ts
console.log('done')# Bun: ~6ms
time bun run benchmark-startup.ts
# Node.js (with tsx): ~80ms
time npx tsx benchmark-startup.tsThe gap is real and consistent. JSC's multi-tier JIT and Bun's lightweight process initialization both contribute.
Package installs — dramatically faster
bun install is the single most impressive speed improvement. On a cold install of a medium-sized project (500+ dependencies), Bun is often 10-20x faster than npm and 3-5x faster than pnpm. It uses a global cache, hardlinks instead of copies, and resolves dependencies in parallel with native code.
# Clean install of a Next.js project
rm -rf node_modules
# npm: ~18s
time npm install
# pnpm: ~8s
time pnpm install
# bun: ~1.2s
time bun installThese numbers vary by project and network conditions, but the magnitude is consistent. For monorepos with multiple node_modules directories, the difference is even more dramatic.
HTTP throughput — it depends
Bun's HTTP benchmarks show impressive requests-per-second numbers for simple "hello world" servers. But most real applications aren't bottlenecked by HTTP parsing—they're bottlenecked by database queries, external API calls, and business logic. When I tested a realistic API that queries PostgreSQL and serializes JSON, the throughput difference between Bun and Node.js (with Fastify) was around 10-15%, not the 3-5x that the synthetic benchmarks suggest.
The speed advantage is real for CPU-bound work in the runtime layer—parsing, serialization, crypto. For I/O-bound applications where you're waiting on the network or disk, the runtime overhead is a small fraction of total latency.
My takeaway: Bun's speed is most impactful for developer experience (installs, startup, test runs) and less impactful for production throughput of typical web applications. That's still valuable—developer experience compounds across a team—but it's not the "everything is 10x faster" story.
Built-in APIs That Replace Dependencies
One of Bun's strongest design choices is shipping batteries-included APIs that replace common npm dependencies. Every dependency you don't install is a dependency you don't maintain, audit, or wait to resolve.
File I/O
// Bun — no imports needed
const file = Bun.file('./data.json')
const data = await file.json()
await Bun.write('./output.txt', 'Hello, world')// Node.js
import { readFile, writeFile } from 'node:fs/promises'
const raw = await readFile('./data.json', 'utf-8')
const data = JSON.parse(raw)
await writeFile('./output.txt', 'Hello, world')Bun.file() returns a lazy reference—it doesn't read the file until you call .text(), .json(), .arrayBuffer(), or .stream(). This is a cleaner API than Node's readFile, which eagerly reads the entire file into memory.
Password hashing
// Bun — built-in, no dependency
const hash = await Bun.password.hash('my-password', {
algorithm: 'argon2id',
memoryCost: 65536,
timeCost: 2,
})
const isValid = await Bun.password.verify('my-password', hash)// Node.js — requires bcrypt or argon2 npm package
import argon2 from 'argon2'
const hash = await argon2.hash('my-password', {
type: argon2.argon2id,
memoryCost: 65536,
timeCost: 2,
})
const isValid = await argon2.verify(hash, 'my-password')Bun.password supports bcrypt and argon2id out of the box. No native addon compilation, no node-gyp issues, no platform-specific binaries. For a feature that almost every web application needs, having it built in is a meaningful reduction in friction.
SQLite
// Bun — built-in SQLite
import { Database } from 'bun:sqlite'
const db = new Database('./app.db')
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
`)
const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
insert.run('Alice', 'alice@example.com')
const users = db.query('SELECT * FROM users').all()
console.log(users)// Node.js — requires better-sqlite3 npm package
import Database from 'better-sqlite3'
const db = new Database('./app.db')
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
`)
const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
insert.run('Alice', 'alice@example.com')
const users = db.prepare('SELECT * FROM users').all()
console.log(users)The APIs are nearly identical—Bun's bun:sqlite was modeled after better-sqlite3. But better-sqlite3 is a native addon that requires compilation and occasionally breaks across Node versions or platforms. Bun's version is built into the runtime. No install issues, no prebuild downloads, no node-gyp.
Test runner
// Bun — built-in test runner (bun:test)
import { expect, test, describe } from 'bun:test'
describe('math', () => {
test('addition', () => {
expect(1 + 1).toBe(2)
})
test('async operation', async () => {
const result = await Promise.resolve(42)
expect(result).toBe(42)
})
})Run with bun test. The API is Jest-compatible—describe, test, expect, beforeEach, afterEach, mocks, snapshots. The speed difference is significant: Bun's test runner starts in milliseconds where Jest takes seconds to bootstrap. For a test suite of 200+ tests, I've seen Bun finish in under a second where Jest takes 8-10 seconds.
Node.js Compatibility
Bun's stated goal is to be a drop-in replacement for Node.js. In practice, it's close but not complete.
What works well:
- Most npm packages install and run without changes
node:fs,node:path,node:crypto,node:url,node:buffer— the core modules that 90% of code depends on- Express, Fastify, Hono — popular HTTP frameworks
- Next.js — Bun can run
next devandnext build - Prisma, Drizzle — ORMs work
package.jsonscripts,node_modulesresolution
What has rough edges:
node:vm— the sandboxed execution module has incomplete support. Libraries that depend on it (some template engines, some test frameworks) may break.- Native addons — packages that compile C++ via
node-gypdon't work unless Bun has a built-in replacement or the package offers a WASM fallback. This affects packages likesharp,canvas, and some database drivers. - Streams — Node's stream implementation is complex and Bun's compatibility isn't 100%. Most common patterns work, but edge cases in
Transformstreams orpipeline()can behave differently. node:cluster— not fully implemented. If you rely on cluster mode for multi-process scaling, you'll need to use a different approach.
The compatibility gap is shrinking with every release. Bun ships updates frequently—often weekly—and each release closes more gaps. But if you're migrating a large production Node.js application, test thoroughly. The issues tend to surface in the dependencies you don't control, not in your own code.
// A practical compatibility check: try your app's test suite
// If your tests pass under Bun, your app is likely compatible
// package.json
{
"scripts": {
"test": "bun test",
"test:node": "jest"
}
}Bun vs Deno vs Node.js
After spending time with all three, here's how I think about the tradeoffs:
Node.js is the safe default. Fifteen years of production use, the largest package ecosystem in any language, battle-tested in every conceivable environment. If you're building something that needs to run reliably for years with a team that rotates, Node.js has the most documentation, the most Stack Overflow answers, and the most hiring pool. The cost: tooling fragmentation (you need separate tools for TypeScript, testing, bundling, linting) and some legacy API design decisions.
Deno prioritizes correctness and security. Permissions by default (your script can't access the network or file system unless you explicitly allow it), web-standard APIs everywhere, TypeScript native, and a thoughtful standard library. Deno feels like what you'd design if you started from scratch with modern web standards. The cost: smaller ecosystem (though npm compatibility has improved dramatically), and the permissions model adds friction for quick scripts.
Bun prioritizes speed and developer experience. The all-in-one toolchain means fewer tools to configure. The startup speed makes scripts and dev workflows feel instant. The built-in APIs reduce dependency count. The cost: youngest of the three, smallest community, most likely to have compatibility gaps, and the least battle-tested in production.
What I'd pick for different scenarios
Production API serving millions of requests: Node.js. The ecosystem maturity, debugging tools (--inspect, Chrome DevTools, clinic.js), APM integrations, and operational knowledge are unmatched. The performance difference between Node.js with Fastify and Bun is small enough that reliability and tooling matter more.
CLI tool or developer utility: Bun. Startup speed dominates the user experience of CLI tools. A tool that starts in 6ms feels instant; one that starts in 80ms feels sluggish. Bun's built-in TypeScript support means zero config.
Quick script or automation: Bun or Deno. Both run TypeScript directly. Deno's permissions model is nice for scripts you download from the internet. Bun's speed is nice for scripts you run frequently.
Monorepo package management: Bun's package manager, then use whatever runtime you want. bun install is fast enough to change how you think about CI caching. You can use bun install and still run your app with Node.js.
Edge functions or serverless: Depends on the platform. Cloudflare Workers has its own runtime. Vercel Edge uses a V8-based runtime. If the platform supports Bun (some do), the cold start advantage is meaningful. Otherwise, you're constrained by the platform's runtime.
Rough Edges
I want to be honest about the things that gave me pause.
Debugging is less mature. Node.js has --inspect with Chrome DevTools, VS Code integration, clinic.js for profiling, and years of tooling built around V8's inspector protocol. Bun supports --inspect and the WebKit inspector, but the ecosystem of profiling and debugging tools is thinner. When something goes wrong in production, the depth of tooling matters.
Error messages can be cryptic. When Bun hits an incompatibility with a Node.js API, the error isn't always clear about what's unsupported. I've spent time debugging issues that turned out to be "this Node.js API isn't fully implemented yet" rather than bugs in my code.
The pace of change is a double-edged sword. Bun ships updates constantly, which means bugs get fixed fast—but it also means behavior can change between versions. Pinning your Bun version in CI is essential. I've had builds break after a minor version bump because a previously-lenient behavior became strict.
Community and ecosystem size. When you hit a Bun-specific issue, there are fewer blog posts, fewer Stack Overflow answers, and fewer people who've encountered it before. The Discord is active and the team is responsive, but it's not the same as Node.js's fifteen years of accumulated knowledge.
Native addon story. If your project depends on packages with native C++ addons (image processing with sharp, PDF generation with certain libraries, some database drivers), verify they work with Bun before committing. Bun is adding N-API compatibility, but it's not complete.
When I'd Reach for Bun
After a few weeks of building with it, here's my practical framework:
Use Bun as your package manager today. Even if you run your app with Node.js, bun install is a drop-in replacement for npm install that's dramatically faster. It uses the same package.json and node_modules structure. This is the lowest-risk way to benefit from Bun immediately.
Use Bun for scripts and tooling. TypeScript execution with zero config, fast startup, built-in test runner. For internal tools, code generators, and automation scripts, Bun is a clear win.
Use Bun for new projects where you control the stack. If you're starting fresh and can choose your dependencies carefully (avoiding native addons that aren't supported), Bun's developer experience is excellent. The built-in APIs for file I/O, hashing, SQLite, and HTTP reduce your dependency surface area.
Be cautious about migrating large Node.js applications. Test your full test suite under Bun. Check your dependency tree for native addons. Verify your deployment platform supports Bun. The migration can be smooth, but the edge cases are real.
Keep Node.js for production systems where reliability is non-negotiable. If you're running financial transactions, healthcare systems, or anything where a subtle runtime incompatibility could cause data loss, Node.js's maturity is worth the slower startup time.
Bun isn't trying to be a marginal improvement over Node.js. It's a rethink of what a JavaScript runtime should include and how fast it should be. Some of that ambition has already delivered—the package manager and startup speed are genuinely transformative. Some of it is still catching up—Node.js compatibility and production tooling need more time. But the trajectory is clear, and ignoring Bun at this point would be like ignoring TypeScript in 2016. Even if you don't adopt it today, understanding what it does and where it's headed makes you a better-informed engineer.