← Blog
Jun 1, 2024

Bun vs Node: Why We Switched Our Runtime

The decision to switch from Node.js to Bun. Performance benchmarks, compatibility testing, and lessons from the migration.

bunnodejsjavascriptperformanceruntime

We were happy Node.js users for years. Then we tried Bun.

Three weeks later, our entire stack runs on Bun.

The Promise

Bun promises:

But promises are cheap. We tested.

Benchmark Results

Installation Speed

# Node.js project (1,200 dependencies)
npm install    # 45 seconds
pnpm install   # 22 seconds
bun install    # 4 seconds

Winner: Bun (11x faster than npm)

Script Execution

# Simple script
node script.js   # 180ms
bun run script.js  # 45ms

# TypeScript (no pre-compile)
ts-node script.ts    # 1,200ms
bun run script.ts   # 60ms

Winner: Bun (4x for JS, 20x for TS)

HTTP Server Performance

// Simple HTTP server benchmark
// 10,000 requests, keep-alive

// Node.js (Express)
Requests/sec: 12,000
Latency avg: 8ms

// Bun (Bun.serve)
Requests/sec: 85,000
Latency avg: 1ms

Winner: Bun (7x more requests)

Build Time

# Build React app
npm run build    # 28 seconds
bun run build    # 12 seconds

Winner: Bun (2.3x faster)

Test Suite

# 500 tests
npm test   # 18 seconds
bun test   # 5 seconds

Winner: Bun (3.6x faster)

Compatibility Testing

What Worked Immediately

What Required Changes

Native modules: Some needed recompilation

bun add sharp  # Handles automatically

Node-specific APIs: Some edge cases

// Node
process.nextTick(callback);

// Bun (works, but setImmediate is preferred)
setImmediate(callback);

File watching: Different behavior in some cases

// Use Bun's native watch
import { watch } from "fs";
// vs Bun-native
Bun.file(path).watch(callback);

What Didn’t Work

Overall compatibility: ~95% of our code worked unchanged.

Migration Process

Step 1: Test Compatibility

# Replace node_modules
rm -rf node_modules package-lock.json
bun install

# Run tests
bun test

Step 2: Update Scripts

// package.json
{
  "scripts": {
    "dev": "bun run --watch src/index.ts",
    "build": "bun build src/index.ts --outdir dist",
    "test": "bun test",
    "start": "bun run dist/index.js"
  }
}

Step 3: Update CI/CD

# GitHub Actions
- uses: oven-sh/setup-bun@v1
  with:
    bun-version: latest
    
- run: bun install
- run: bun test
- run: bun run build

Step 4: Docker Updates

FROM oven/bun:1 as builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile

COPY . .
RUN bun run build

FROM oven/bun:1-slim
COPY --from=builder /app/dist /app
CMD ["bun", "run", "/app/index.js"]

Real-World Impact

Developer Experience

Metric Before (Node) After (Bun)
Install time 45s 4s
Dev server start 3s 0.5s
Hot reload 500ms 100ms
Test suite 18s 5s
Build time 28s 12s

Total daily time saved per developer: ~15 minutes

Production Performance

Metric Before (Node) After (Bun)
Cold start 800ms 150ms
Requests/sec 12,000 85,000
P99 latency 45ms 8ms
Memory usage 180MB 120MB

Cost Impact

Gotchas We Discovered

1. Bun’s Bundler Is Young

For complex bundling, we still use esbuild in some places.

2. Error Messages Differ

Stack traces look different. Takes adjustment.

3. Some Node.js Patterns Don’t Translate

// Node pattern that works differently
const module = require(`./dynamic/${name}`);

// Bun prefers
const module = await import(`./dynamic/${name}`);

4. Lock File Differences

bun.lockb is binary (faster), but you lose diff visibility.

When to Stick with Node

Our Recommendation

For new projects: Start with Bun. The benefits are significant.

For existing projects: Test compatibility, migrate incrementally.

For production: We’ve run Bun in production for 6 months with zero issues.


Bun powers our development workflow. For the full tech stack, see Building a 40+ Platform Ecosystem.

All posts Work with me