- Published on
Migrating From Node.js to Bun — What Works, What Breaks, and Benchmarks
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
Bun claims Node.js compatibility, but the reality is nuanced. Some apps migrate with zero changes; others hit friction with native modules or ESM edge cases. This guide covers compatibility status, performance benchmarks, and a safe migration strategy for production codebases.
- Bun's Node.js Compatibility Layer
- Package Management Speed
- HTTP Servers: Bun.serve() vs Express
- Native Database Drivers
- Built-In Test Runner
- What Doesn't Work Yet
- Performance Benchmarks
- Gradual Migration Strategy
- Compatibility Layer for Express
- Production Readiness Checklist
- When to Migrate to Bun
- Checklist
- Conclusion
Bun's Node.js Compatibility Layer
Bun ships with a Node.js compatibility layer via the node: protocol:
// Works on Bun and Node
import * as fs from 'node:fs'
import * as path from 'node:path'
import * as crypto from 'node:crypto'
console.log(fs.readFileSync('file.txt', 'utf-8'))
Core modules like fs, path, crypto, http, and stream are fully implemented. However, some modules have partial support or behavioral differences.
Fully Supported: fs, path, crypto, util, buffer, events, stream, zlib, http, https, net, dgram, dns
Partial Support: cluster (no clustering, runs single process), child_process (limited), net (some options missing)
Unsupported: v8, vm (some APIs), worker_threads (different implementation)
Package Management Speed
bun install is dramatically faster than npm or yarn:
# npm install: ~45s (with 200 dependencies)
time npm install
# yarn install: ~38s
time yarn install
# bun install: ~2.5s
time bun install
Bun parallelizes all operations and reuses cached packages. On monorepos with 50+ packages, the difference is staggering.
Lock file format:
# bun.lock
[[packages]]
name = "lodash"
version = "4.17.21"
resolution = "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#..."
Bun's lock file is deterministic and human-readable.
HTTP Servers: Bun.serve() vs Express
Bun provides a native HTTP server significantly faster than Express:
// Bun native — 65,000 req/s
export default {
fetch(request: Request) {
return new Response('Hello Bun')
},
port: 3000,
}
// Express on Node — 12,000 req/s
import express from 'express'
const app = express()
app.get('/', (req, res) => res.send('Hello Express'))
app.listen(3000)
Bun.serve() uses no external dependencies. The HTTP parser is built into the runtime.
Native Database Drivers
Bun ships with native drivers for SQLite and PostgreSQL:
import { Database } from 'bun:sqlite'
const db = new Database('data.db')
db.prepare('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)').run()
const stmt = db.prepare('INSERT INTO users (name) VALUES (?)')
stmt.run('Alice')
const users = db.prepare('SELECT * FROM users').all()
console.log(users)
Direct SQLite queries execute in <1ms. No serialization layer like in Node.js ORMs.
PostgreSQL:
const client = await Bun.sql`SELECT version()`
const result = await client.rows()
console.log(result)
Native drivers are opt-in. If you''re using an ORM, this matters less.
Built-In Test Runner
Bun includes a test runner—no Jest config needed:
import { describe, it, expect } from 'bun:test'
describe('Math', () => {
it('adds numbers', () => {
expect(1 + 1).toBe(2)
})
})
Run with bun test. Tests execute in parallel at native speed.
What Doesn't Work Yet
Native C++ Modules: Node's node-gyp modules (like bcrypt, canvas, sqlite3) don't work in Bun. Use pure JavaScript alternatives:
bcrypt→@noble/hashesorargon2sqlite3→ usebun:sqliteinsteadcanvas→ no direct substitute yet
ESM Edge Cases: Default exports from CJS packages sometimes fail:
// This works
import pino from 'pino'
// This sometimes breaks (check Bun docs)
import * as pino from 'pino'
Certain Node APIs: Worker threads, vm.Script, and cluster mode have limitations.
Performance Benchmarks
Throughput (req/s, 12 connections):
| Framework | Runtime | Throughput | Startup |
|---|---|---|---|
| Native | Bun | 65,000 | <5ms |
| Elysia | Bun | 64,000 | <10ms |
| Hono | Bun | 62,000 | <8ms |
| Fastify | Node 22 | 28,000 | 150ms |
| Express | Node 22 | 12,000 | 180ms |
Memory usage (idle):
- Bun runtime: 25MB
- Elysia API: 20-30MB
- Node.js runtime: 35MB
- Express API: 60-90MB
Bun''s garbage collector is aggressive—it reclaims memory faster than Node.
Gradual Migration Strategy
Don't rip-and-replace. Migrate incrementally:
Phase 1: Audit Dependencies
# Check which packages are Bun-compatible
npm ls --all | grep gyp # Find native modules
Replace problematic packages before migrating. For example:
bcrypt→@noble/bcryptuuid→ Bun''s built-incrypto.randomUUID()
Phase 2: Local Testing
# Install Bun
curl -fsSL https://bun.sh/install | bash
# Install and test locally
bun install
bun run src/index.ts
Test your app thoroughly. Run your full test suite:
bun test
Phase 3: Containerization
Use Bun''s official Docker image:
FROM oven/bun:1.0
WORKDIR /app
COPY . .
RUN bun install --production
EXPOSE 3000
CMD ["bun", "run", "src/index.ts"]
Build and test in staging before production.
Phase 4: Canary Deployment
Deploy to 5-10% of traffic first. Monitor:
- Error rates
- Latency percentiles (p50, p95, p99)
- Memory usage
- CPU usage
Only ramp up after 2-4 hours of stable metrics.
Compatibility Layer for Express
If you''re running Express on Bun, create an adapter:
import express from 'express'
const app = express()
app.use(express.json())
app.get('/api/users', (req, res) => {
res.json([{ id: 1, name: 'Alice' }])
})
// For Bun
export default {
async fetch(req: Request) {
// Wrap Express to handle Bun's Request/Response
return new Promise((resolve) => {
const nodeReq = new IncomingMessage(/* ... */)
const nodeRes = new ServerResponse(nodeReq)
app(nodeReq, nodeRes)
nodeRes.on('finish', () => {
resolve(new Response(nodeRes.body))
})
})
},
port: 3000,
}
This is the fallback if native Bun adapters aren''t available.
Production Readiness Checklist
- All dependencies are Bun-compatible or have replacements
- Full test suite passes on Bun
- Benchmarks meet SLA thresholds
- Error handling is identical to Node.js version
- Database drivers are configured (native or ORM)
- Logging captures all errors
- Health checks pass
- Graceful shutdown works
- Monitoring and tracing are configured
- Rollback plan is documented
When to Migrate to Bun
Migrate if: You''re greenfielding a new API, performance is critical, and your stack is modern (TypeScript, no native modules).
Stay with Node.js if: You have large monoliths, heavy native module usage (bcrypt, canvas), or your team lacks experience with Bun.
Bun is production-ready for new projects. Migration of existing systems is lower-risk if they''re already modern.
Checklist
- Check Bun compatibility of all dependencies
- Audit native module usage
- Run test suite locally on Bun
- Benchmark your app on both runtimes
- Plan canary deployment strategy
- Configure observability and monitoring
- Test graceful shutdown and error handling
- Document rollback procedure
Conclusion
Bun is not Node.js, but it''s Node-compatible enough for most applications. The performance gains are real—2-5x throughput and sub-10ms startup times are achievable. For new projects, Bun should be your default. For existing systems, migrate only if the pain of native modules is acceptable. The ecosystem is maturing rapidly; within 12 months, Bun compatibility will be the norm, not the exception.