Migrating from Node.js to Bun 1.2 in Production: Full-Stack HTTP, SQLite & Package Performance

Table of Contents(22 sections)
This guide details the process of migrating existing Node.js microservices to Bun 1.2 for production deployment. We will cover HTTP server migration, bun:sqlite integration, WebSocket server implementation, npm package compatibility, native C++ addon considerations, Node.js API gap mitigation, and Docker containerization with performance benchmarks.
Bun 1.2 Core Advantages for Production
Bun 1.2 offers significant performance advantages over Node.js, primarily due to its underlying Zig implementation and JavaScriptCore engine. Key benefits include:
- Faster Startup Times: Critical for serverless functions and frequently scaled microservices.
- Reduced Memory Footprint: Lower operational costs and higher density per host.
- Integrated Tooling:
bun install,bun run,bun test, andbun buildstreamline development workflows. - Native APIs:
bun:sqlite,bun:ffi,bun:serveprovide highly optimized primitives.
HTTP Server Migration
Bun's native HTTP server (Bun.serve) is a high-performance alternative to Node.js's http module or frameworks like Express. It's designed for minimal overhead.
Node.js (Express) Example
// src/node-server.ts
import express from 'express';
import { readFileSync } from 'fs';
import path from 'path';
const app = express();
const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;
app.use(express.json());
app.get('/', (req, res) => {
res.send('Hello from Node.js Express!');
});
app.get('/data', (req, res) => {
try {
const data = readFileSync(path.join(__dirname, '../data.json'), 'utf8');
res.json(JSON.parse(data));
} catch (error) {
console.error('Error reading data:', error);
res.status(500).send('Internal Server Error');
}
});
app.listen(port, () => {
console.log(`Node.js Express server listening on port ${port}`);
});
Bun 1.2 (Bun.serve) Equivalent
Migrating to Bun.serve involves adapting request/response handling. Bun's Request and Response objects are Web-standard Request and Response objects.
// src/bun-server.ts
import { readFileSync } from 'fs';
import path from 'path';
const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;
const server = Bun.serve({
port,
async fetch(req: Request): Promise<Response> {
const url = new URL(req.url);
if (url.pathname === '/') {
return new Response('Hello from Bun!', { status: 200 });
}
if (url.pathname === '/data') {
try {
const dataPath = path.join(import.meta.dir, '../data.json');
const data = readFileSync(dataPath, 'utf8');
return new Response(data, {
headers: { 'Content-Type': 'application/json' },
status: 200,
});
} catch (error) {
console.error('Error reading data:', error);
return new Response('Internal Server Error', { status: 500 });
}
}
// Example for POST request with JSON body
if (url.pathname === '/submit' && req.method === 'POST') {
try {
const body = await req.json();
console.log('Received JSON body:', body);
return new Response(JSON.stringify({ message: 'Data received', data: body }), {
headers: { 'Content-Type': 'application/json' },
status: 200,
});
} catch (error) {
console.error('Error parsing JSON body:', error);
return new Response('Bad Request: Invalid JSON', { status: 400 });
}
}
return new Response('Not Found', { status: 404 });
},
error(error: Error): Response {
console.error('Bun server error:', error);
return new Response('Internal Server Error', { status: 500 });
},
});
console.log(`Bun server listening on port ${server.port}`);
Note the use of import.meta.dir for path resolution, which is Bun's equivalent to Node.js's __dirname.
Native SQLite with bun:sqlite
Bun provides a highly optimized, native SQLite client via bun:sqlite. This eliminates the need for external npm packages like sqlite3 or better-sqlite3, which often involve native C++ addons and compilation steps.
Node.js (better-sqlite3) Example
// src/node-sqlite.ts
import Database from 'better-sqlite3';
import path from 'path';
const dbPath = path.join(__dirname, '../data.db');
const db = new Database(dbPath);
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);
`);
export function insertUser(name: string, email: string) {
const stmt = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
const info = stmt.run(name, email);
return info.lastInsertRowid;
}
export function getUsers() {
const stmt = db.prepare('SELECT * FROM users');
return stmt.all();
}
// Example usage
if (require.main === module) {
insertUser('Alice', 'alice@example.com');
insertUser('Bob', 'bob@example.com');
console.log('Users:', getUsers());
db.close();
}
Bun 1.2 (bun:sqlite) Equivalent
bun:sqlite offers a similar API surface, making migration straightforward.
// src/bun-sqlite.ts
import { Database } from 'bun:sqlite';
import path from 'path';
const dbPath = path.join(import.meta.dir, '../data.db');
const db = new Database(dbPath);
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);
`);
export function insertUser(name: string, email: string) {
const query = db.query('INSERT INTO users (name, email) VALUES (?, ?)');
const result = query.run(name, email);
// Bun's result object for INSERT operations might differ slightly
// For SQLite, lastInsertRowid is often available on the database object or via PRAGMA
// For simplicity, we'll assume a successful run implies insertion.
// A more robust solution would query for the last_insert_rowid()
const lastIdQuery = db.query('SELECT last_insert_rowid() as id;');
return (lastIdQuery.get() as { id: number }).id;
}
export function getUsers() {
const query = db.query('SELECT * FROM users');
return query.all();
}
// Example usage
if (import.meta.main) {
insertUser('Charlie', 'charlie@example.com');
insertUser('David', 'david@example.com');
console.log('Users:', getUsers());
db.close();
}
Native WebSocket Servers
Bun's Bun.serve also includes first-class support for WebSockets, providing a high-performance, integrated solution.
Node.js (ws library) Example
// src/node-ws.ts
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', ws => {
console.log('Client connected (Node.js WS)');
ws.on('message', message => {
console.log(`Received: ${message}`);
ws.send(`Echo: ${message}`);
});
ws.on('close', () => console.log('Client disconnected (Node.js WS)'));
ws.on('error', error => console.error('WebSocket error (Node.js WS):', error));
});
console.log('Node.js WebSocket server listening on port 8080');
Bun 1.2 (Bun.serve WebSocket) Equivalent
Bun's WebSocket API is integrated directly into Bun.serve, using a websocket configuration object.
// src/bun-ws.ts
const server = Bun.serve({
port: 8080,
fetch(req, server) {
const url = new URL(req.url);
if (url.pathname === '/ws') {
const success = server.upgrade(req, {
data: {
// Optional: attach any data to the WebSocket connection
connectedAt: Date.now(),
},
});
if (success) {
return; // Bun handles the WebSocket connection
}
}
return new Response('Not Found', { status: 404 });
},
websocket: {
open(ws) {
console.log(`Client connected (Bun WS). Connected at: ${ws.data.connectedAt}`);
ws.send('Welcome to Bun WebSocket!');
},
message(ws, message) {
console.log(`Received: ${message}`);
ws.send(`Echo: ${message}`);
},
close(ws, code, message) {
console.log(`Client disconnected (Bun WS). Code: ${code}, Message: ${message}`);
},
error(ws, error) {
console.error('WebSocket error (Bun WS):', error);
},
// Optional: perMessageDeflate, maxPayloadLength, idleTimeout
// idleTimeout: 30, // seconds
},
});
console.log(`Bun WebSocket server listening on port ${server.port}`);
npm Package Compatibility and Native C++ Addons
Bun aims for high compatibility with existing npm packages. Most pure JavaScript packages will work without modification.
Native C++ Addons
This is where compatibility can be challenging. Node.js native addons (.node files) are compiled against Node.js's N-API (Node-API) and V8 engine. Bun uses JavaScriptCore and its own FFI (Foreign Function Interface) for native interactions.
- Direct Compatibility: Native C++ addons compiled for Node.js are generally not directly compatible with Bun.
- Workarounds:
- Pure JS Alternatives: Look for pure JavaScript alternatives to packages that rely on native addons.
bun:ffi: For critical performance-sensitive operations, you might need to rewrite the native logic usingbun:ffito call into a shared library (.so,.dylib,.dll) compiled specifically for Bun. This is a complex undertaking.- Containerization: If a specific native addon is indispensable and cannot be rewritten, you might consider running that specific microservice in a Node.js container and communicating with it via RPC. This negates some Bun benefits but ensures functionality.
Example: bcrypt (Native Addon)
bcrypt is a common package that uses native C++ addons for performance.
// src/node-bcrypt.ts
import bcrypt from 'bcrypt';
async function hashPassword(password: string) {
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
console.log('Hashed password (Node.js):', hashedPassword);
return hashedPassword;
}
async function verifyPassword(password: string, hash: string) {
const isMatch = await bcrypt.compare(password, hash);
console.log('Password match (Node.js):', isMatch);
return isMatch;
}
if (require.main === module) {
(async () => {
const hash = await hashPassword('mysecretpassword');
await verifyPassword('mysecretpassword', hash);
await verifyPassword('wrongpassword', hash);
})();
}
When running bun install bcrypt, Bun will attempt to install it. If it fails to build the native addon, you'll see an error. For such cases, you might need to use a pure JS alternative or a different approach.
Bun 1.2 (bcrypt with potential issues)
Bun can sometimes run Node.js native addons if they are pre-compiled for the correct architecture and N-API version, but this is not guaranteed and often fails. A safer approach is to use a pure JS alternative or a different library.
For bcrypt, a pure JS implementation like bcryptjs is often a viable fallback, though it will be slower.
// src/bun-bcryptjs.ts
import bcrypt from 'bcryptjs'; // Using bcryptjs for Bun compatibility
async function hashPassword(password: string) {
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
console.log('Hashed password (Bun/bcryptjs):', hashedPassword);
return hashedPassword;
}
async function verifyPassword(password: string, hash: string) {
const isMatch = await bcrypt.compare(password, hash);
console.log('Password match (Bun/bcryptjs):', isMatch);
return isMatch;
}
if (import.meta.main) {
(async () => {
const hash = await hashPassword('mysecretpassword');
await verifyPassword('mysecretpassword', hash);
await verifyPassword('wrongpassword', hash);
})();
}
Node.js API Gaps and Mitigation
While Bun aims for high Node.js compatibility, some APIs are not fully implemented or behave differently.
child_process: Most common functions likeexec,spawn,forkare supported. Edge cases or specific options might differ.vmmodule: Limited support. If your application heavily relies onvm.runInContextor similar, this might be a blocker.domainmodule: Deprecated in Node.js, not supported in Bun.clustermodule: Bun does not have a direct equivalent forcluster. For multi-core utilization, you'd typically run multiple Bun processes and use a load balancer, or leverage Bun's built-in multi-threading for certain tasks (though not for HTTP server scaling in the same wayclusterworks).fsmodule: Largely compatible, but some less common options or synchronous variants might have subtle differences. Always test file system heavy operations.net/tls: Basic client/server functionality is present, but advanced configurations might require adjustments.
Mitigation Strategy:
- Comprehensive Testing: Unit, integration, and end-to-end tests are crucial to identify API gaps.
- Polyfills/Shims: For minor gaps, a small polyfill might be possible.
- Refactoring: For significant gaps, refactor the problematic code to use Web-standard APIs or Bun's native APIs.
- Feature Flags: Isolate problematic code paths with feature flags, allowing a gradual migration.
Docker Containerization Benchmarks
Containerizing Bun applications is straightforward. The official oven/bun Docker images provide a lean base.
Dockerfile for Node.js
# Dockerfile.node
FROM node:20-alpine AS base
WORKDIR /app
COPY package.json bun.lockb* ./
RUN npm install --frozen-lockfile
COPY . .
EXPOSE 3000
CMD ["node", "src/node-server.ts"]
Dockerfile for Bun
# Dockerfile.bun
FROM oven/bun:1.2.0-alpine AS base
WORKDIR /app
COPY package.json bun.lockb* ./
# Bun automatically installs dependencies on 'bun run' if not present,
# but explicit install is good practice for build stage.
RUN bun install --frozen-lockfile
COPY . .
EXPOSE 3000
CMD ["bun", "run", "src/bun-server.ts"]
Benchmarking Methodology
We'll compare image size and runtime memory usage for a simple HTTP server.
- Build Images:
bash
docker build -t node-app -f Dockerfile.node . docker build -t bun-app -f Dockerfile.bun . - Image Size:
bash
docker images | grep "node-app\|bun-app" - Runtime Memory (using
docker stats):bashdocker run -d --name node-server -p 3001:3000 node-app docker run -d --name bun-server -p 3002:3000 bun-app docker stats --no-stream node-server bun-server # Stop containers after collecting stats docker stop node-server bun-server && docker rm node-server bun-server
Benchmark Results (Illustrative)
| Metric | Node.js (20-alpine) | Bun (1.2.0-alpine) | Notes |
|---|---|---|---|
| Image Size | ~180 MB | ~100 MB | Bun's base image is significantly smaller. |
| Startup Time | ~500 ms | ~50 ms | Bun is orders of magnitude faster. |
| Memory Usage | ~30 MB | ~10 MB | For a simple HTTP server, Bun uses less. |
| RPS (ab -c 50 -n 10000) | ~2500 RPS | ~7000 RPS | Bun.serve is highly optimized. |
Note: These are illustrative benchmarks. Actual results depend heavily on application complexity, workload, and hardware.
Production Gotchas & Troubleshooting
-
Error: Cannot find module '...':- Cause: Bun's module resolution is generally compatible but can differ for specific edge cases or if
tsconfig.jsonpaths are not correctly configured for Bun. - Fix:
- Ensure
tsconfig.jsonpathsare correctly mapped andbaseUrlis set. - Verify
package.jsonexportsfield for imported packages. - Check for missing
bun installornpm installif using a mix. - Bun's
node_modulesresolution can be stricter; ensure all dependencies are explicitly listed. - If using
require(), ensure the file extension is present (e.g.,require('./module.js')).
- Ensure
- Cause: Bun's module resolution is generally compatible but can differ for specific edge cases or if
-
Native Addon Failures:
- Cause: Attempting to use a Node.js native C++ addon (
.nodefile) directly with Bun. - Fix:
- Identify the problematic package.
- Search for a pure JavaScript alternative (e.g.,
bcryptjsinstead ofbcrypt). - If no alternative, consider isolating that functionality into a separate Node.js microservice or reimplementing with
bun:ffi(advanced).
- Cause: Attempting to use a Node.js native C++ addon (
-
process.envDifferences:- Cause: Bun's
process.envis largely compatible, but some Node.js-specific environment variables might not be present or behave identically. - Fix:
- Explicitly define required environment variables in your deployment environment.
- Avoid relying on highly Node.js-specific internal environment variables.
- Cause: Bun's
-
BufferAPI Inconsistencies:- Cause: While Bun supports
Buffer, its underlying implementation might have subtle differences from Node.js's, especially with older or less commonBuffermethods. - Fix:
- Prioritize Web-standard
Uint8ArrayandTextEncoder/TextDecoderwhere possible. - Thoroughly test code paths involving
Buffermanipulation.
- Prioritize Web-standard
- Cause: While Bun supports
-
fs.watch/fs.watchFileBehavior:- Cause: File system watching can be platform-dependent and have different performance characteristics or event triggers between Node.js and Bun.
- Fix:
- Test file watching extensively in your target production environment.
- Consider external file watching solutions if native behavior is inconsistent.
-
Bun.servevs. Node.js HTTP ServerkeepAliveTimeout:- Cause: Bun's
Bun.servehas anidleTimeoutfor WebSockets and a general connection timeout. Node.js haskeepAliveTimeout. Default values and behavior might differ. - Fix:
- Explicitly configure
idleTimeoutinBun.servefor WebSockets. - For HTTP, ensure your load balancer or proxy handles connection timeouts gracefully, or implement custom timeout logic within your
fetchhandler if needed.
- Explicitly configure
- Cause: Bun's
Frequently Asked Questions
-
Q: Can I use existing Node.js frameworks like Express or NestJS with Bun?
- A: Yes, largely. Bun aims for high Node.js compatibility, so many frameworks will run. However, you won't get the full performance benefits of
Bun.servefor HTTP handling. For optimal performance, migrate HTTP endpoints toBun.servedirectly or use a Bun-native framework.bun installwill typically handle framework dependencies.
- A: Yes, largely. Bun aims for high Node.js compatibility, so many frameworks will run. However, you won't get the full performance benefits of
-
Q: How does Bun handle TypeScript compilation in production?
- A: Bun has a built-in TypeScript transpiler. You can directly run
.tsfiles withbun run src/index.ts. For production,bun buildcan compile your TypeScript into JavaScript, which can then be run. This eliminates the need fortscorts-node.
- A: Bun has a built-in TypeScript transpiler. You can directly run
-
Q: What about database drivers other than SQLite (e.g., PostgreSQL, MySQL)?
- A: For most relational databases, you'll use existing npm packages (e.g.,
pg,mysql2,sequelize,prisma). These are typically pure JavaScript or rely on standard network protocols, so they generally work well with Bun.bun installwill manage their dependencies.
- A: For most relational databases, you'll use existing npm packages (e.g.,
-
Q: Is
bun:ffia viable replacement for all native Node.js addons?- A:
bun:ffiis powerful but requires significant effort. You need to write C/C++/Rust code, compile it into a shared library (.so,.dylib,.dll), and then define the function signatures in TypeScript forbun:ffito call. It's a solution for performance-critical, isolated native logic, not a drop-in replacement for complex Node.js addons.
- A:
-
Q: How do I debug a Bun application in production?
- A: Bun supports the Chrome DevTools Protocol. You can start Bun with
--inspector--inspect-brk(e.g.,bun --inspect src/index.ts) and connect a debugger (like VS Code's debugger or Chrome DevTools). This provides a familiar debugging experience similar to Node.js.
- A: Bun supports the Chrome DevTools Protocol. You can start Bun with
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Migrating from Redis to Valkey 8 in Production: Zero-Downtime Replication & Latency Benchmarks
Comprehensive guide covering migrating from redis to valkey 8 in production: zero-downtime replication & latency benchmarks with production-grade architecture and code examples.
Read more
Kafka vs Redpanda in 2026: Thread-per-Core Architecture, Zero-Disk Cache & P99 Latency Benchmarks
Comprehensive guide covering kafka vs redpanda in 2026: thread-per-core architecture, zero-disk cache & p99 latency benchmarks with production-grade architecture and code examples.
Read more
Optimizing Python FastAPI for High-Concurrency
A deep dive into maximizing the performance of FastAPI applications for high-concurrency environments, covering Uvicorn, Gunicorn workers, async patterns, and database connection pooling.
Read more