Bun — The Fast JavaScript Runtime That Replaces Node
A practical guide to Bun as a Node.js replacement — what's faster, what's compatible, and whether you should switch your production projects today.
Bun shipped 1.0 in September 2023 and immediately posted benchmark numbers that made Node.js look like it was running through molasses. HTTP requests handled 4x faster. TypeScript executed without a compile step. npm packages installed in under a second. The JavaScript ecosystem collectively raised an eyebrow.
Two years later, Bun is at v1.2+, the compatibility story has improved dramatically, and real teams are running it in production. The question worth answering now isn't whether Bun is fast — it is — but whether it's ready to replace Node.js in your stack.
What Bun Actually Is
Bun is four things in one binary:
- A JavaScript/TypeScript runtime — like Node.js, but built on JavaScriptCore (Safari's engine) instead of V8 (Chrome's engine)
- A package manager — replaces npm, yarn, and pnpm
- A bundler — replaces esbuild, webpack, Rollup for many use cases
- A test runner — replaces Jest and Vitest for unit testing
# Install Bun
curl -fsSL https://bun.sh/install | bash
# Check version
bun --version
# Run TypeScript directly — no tsconfig, no build step
bun run server.ts
# Install packages (dramatically faster than npm)
bun install
# Run tests
bun test
# Bundle for production
bun build ./src/index.ts --outdir ./dist
The Speed Difference Is Real
The performance gap isn't marketing. Bun's HTTP server consistently outperforms Node's by a significant margin in real-world benchmarks.
// server.ts — Bun's built-in HTTP server
const server = Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/api/users") {
return Response.json([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
]);
}
if (url.pathname === "/api/health") {
return new Response("OK");
}
return new Response("Not Found", { status: 404 });
},
});
console.log(Server running at http://localhost:${server.port});
Run it with bun run server.ts. No compilation. No ts-node. No tsx. Just runs.
| Factor | Node.js | Bun |
|---|---|---|
| JS Engine | V8 (Chrome) | JavaScriptCore (Safari) |
| Implementation | C++ | Zig + C |
| Startup time | ~40ms | ~5ms |
| HTTP handling | libuv event loop | io_uring (Linux) / kqueue (macOS) |
| TypeScript | Requires transpilation | Native execution |
| Package install | npm ~30s for fresh install | bun ~3s for same project |
Package Management — The Killer Feature Nobody Expected
Bun's runtime speed gets the headlines, but its package manager might be the more practical reason to adopt it. bun install is absurdly fast.
# Fresh install of a Next.js project (node_modules from scratch)
# npm: 28 seconds
# pnpm: 12 seconds
# bun: 3 seconds
bun install
# Add a package
bun add zod drizzle-orm
# Add dev dependency
bun add -d vitest @types/node
# Remove a package
bun remove lodash
# Update all packages
bun update
Bun uses a global cache and hard links, so packages downloaded once are reused across all projects on your machine without duplication. It reads package.json and generates a bun.lockb (binary lockfile) that's faster to parse than JSON or YAML lockfiles.
The compatibility is solid — bun install reads package.json, respects workspaces, handles peer dependencies, and works with private registries.
// package.json — works identically with bun install
{
"name": "my-app",
"dependencies": {
"hono": "^4.0.0",
"drizzle-orm": "^0.35.0"
},
"devDependencies": {
"typescript": "^5.5.0",
"@types/node": "^22.0.0"
}
}
File I/O and the Bun APIs
Bun provides its own APIs for common operations that are significantly faster than Node's equivalents.
// Reading files
const text = await Bun.file("config.json").text();
const json = await Bun.file("data.json").json();
const buffer = await Bun.file("image.png").arrayBuffer();
// Writing files
await Bun.write("output.txt", "Hello, Bun!");
await Bun.write("data.json", JSON.stringify({ key: "value" }));
// Hashing
const hash = Bun.hash("some string");
const sha256 = new Bun.CryptoHasher("sha256")
.update("password")
.digest("hex");
// SQLite — built in, no npm package needed
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite");
db.run("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)");
db.run("INSERT INTO users (name) VALUES (?)", ["Alice"]);
const users = db.query("SELECT * FROM users").all();
console.log(users);
The built-in SQLite is a genuine game-changer for small services, CLI tools, and local development. No better-sqlite3 installation, no native compilation issues, no node-gyp headaches.
Bun's Test Runner
// math.test.ts
import { expect, test, describe } from "bun:test";
describe("math operations", () => {
test("addition", () => {
expect(2 + 2).toBe(4);
});
test("async fetch", async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const todo = await response.json();
expect(todo.id).toBe(1);
});
test("file reading", async () => {
const file = Bun.file("package.json");
const text = await file.text();
expect(text).toContain("name");
});
});
bun test # Run all tests
bun test --watch # Watch mode
bun test math.test.ts # Specific file
bun test --coverage # With coverage
The API is Jest-compatible. describe, test, expect, beforeEach, afterEach, mock — all work. Most Jest test suites run with minimal changes.
Node.js Compatibility
This is where it gets nuanced. Bun aims for full Node.js API compatibility, and it's gotten close:
Works well:- Express, Fastify, Koa
- Most npm packages
fs,path,crypto,child_processfetch,WebSocket,Request/Response.envfiles (loaded automatically)- Workspaces and monorepos
- Some native Node.js addons (C++ addons compiled for V8 may not work on JSC)
vmmodule edge cases- Some
worker_threadsbehaviors differ - Niche
streampatterns may behave differently
// Express works out of the box
import express from "express";
const app = express();
app.use(express.json());
app.get("/api/users", (req, res) => {
res.json([{ id: 1, name: "Alice" }]);
});
app.listen(3000, () => {
console.log("Express on Bun, port 3000");
});
bun run app.ts # Express running on Bun's runtime
When to Use Bun
Good fits:- New projects where you control the stack
- API servers (especially with Bun.serve or Hono)
- CLI tools and scripts (the startup time advantage is massive)
- Monorepos that need fast package installation
- Projects that want built-in TypeScript without configuration
- SQLite-backed services
- Large Node.js codebases with heavy native addon usage
- Projects depending on V8-specific behavior
- Anything using
node:vmextensively - Production systems where the team isn't comfortable debugging runtime differences
A Practical Bun Project Setup
Here's how a real Bun project looks — an API server with SQLite, structured for production.
// src/index.ts
import { Database } from "bun:sqlite";
const db = new Database("app.sqlite");
// Initialize schema
db.run(
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
)
);
const server = Bun.serve({
port: process.env.PORT || 3000,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/api/todos" && req.method === "GET") {
const todos = db.query("SELECT * FROM todos ORDER BY created_at DESC").all();
return Response.json(todos);
}
if (url.pathname === "/api/todos" && req.method === "POST") {
const body = await req.json();
const stmt = db.prepare("INSERT INTO todos (title) VALUES (?) RETURNING *");
const todo = stmt.get(body.title);
return Response.json(todo, { status: 201 });
}
if (url.pathname.startsWith("/api/todos/") && req.method === "DELETE") {
const id = url.pathname.split("/").pop();
db.run("DELETE FROM todos WHERE id = ?", [id]);
return new Response(null, { status: 204 });
}
return new Response("Not Found", { status: 404 });
},
});
console.log(Server running at http://localhost:${server.port});
// package.json
{
"name": "bun-todo-api",
"scripts": {
"dev": "bun --watch src/index.ts",
"start": "bun src/index.ts",
"test": "bun test"
}
}
No build step. No TypeScript configuration. No dependency for SQLite. No Express. The whole thing runs with zero npm packages if you want it to.
Migrating From Node.js
If you're considering a migration, start small:
- Replace npm with bun as your package manager first. This is zero-risk — it reads the same
package.jsonand your code still runs on Node. You just get faster installs.
- Run your test suite on Bun. If tests pass, your codebase is probably compatible. Fix any failures — they'll usually be Node-specific API edge cases.
- Switch the runtime for internal tools and scripts first. CLI tools, build scripts, dev utilities. Low blast radius if something breaks.
- Move API services last. Start with non-critical services, monitor for a few weeks, then expand.
# Step 1: Use Bun for package management only
bun install # Instead of npm install
node src/index.js # Still running on Node
# Step 2: Switch the runtime
bun src/index.ts # Now running on Bun
The Bottom Line
Bun is fast, the developer experience is excellent, and the compatibility story is good enough for most projects. It's not a toy — companies are running it in production. The package manager alone is worth adopting even if you stick with Node.js as your runtime.
The JavaScript runtime landscape finally has real competition, and that's making everything better. Node.js is getting faster too, partly because Bun proved what's possible.
If you're starting a new project, there's little reason not to try Bun. If you're maintaining a large Node.js application, migrate incrementally and test thoroughly. Either way, it's worth having Bun in your toolkit — tools like CodeUp can help you explore the differences hands-on with interactive examples.