Node.js to Bun: migration with real benchmark, compatibility, and rollback
Back to blog

Node.js to Bun: migration with real benchmark, compatibility, and rollback

6/7/2026 · 9 min · Development

Migrating a production workload from Node.js to Bun without a rigorous engineering method is a recipe for swapping performance bottlenecks for critical compatibility incidents. Bun promises extraordinary execution speeds, reduced memory usage, and instant startup times, but its foundation on Safari's JavaScriptCore (JSC) engine - unlike Chrome's V8 engine used by Node.js - introduces behavioral differences and subtle incompatibilities in low-level dependencies.

Below, I detail the complete runbook to perform the transition incrementally, covering native dependency validation, TypeScript support, real benchmarks with SRE monitoring, regression testing, security, CI/CD pipelines, and a robust automatic rollback strategy for production risk mitigation.

Technical CriterionNode.js (V8)Bun (JavaScriptCore - JSC)Transition Impact
Minimum Version18+ (LTS recommended)1.0+ (stable)Stability prerequisite
Startup TimeMedium (slower V8 loading)Instant (optimized JSC)Reduction of Serverless/Cold Start latency
Dependency InstallationNPM / Yarn / PNPM (varying speed)Native (bun install, extremely fast)Drastic reduction of CI/CD build times
TypeScript SupportRequires external transpilation (tsc / esbuild)Native out-of-the-box supportEliminates extra build steps and tools
WebSockets APIRelies on external packages (ws, socket.io)Native, high-performance (Bun.serve)Reduces CPU usage and package overhead
Environment VariablesRequires libraries (dotenv)Native support for .env filesSimplifies project bootstrap

1. Minimum required versions and runtime compatibility#

Before starting any changes, it is mandatory to establish the baseline runtime versions. The migration is only technically feasible and stable if the ecosystem is up to date.

Run the following commands to check the local environment versions:

# Check the active Node.js version
node --version

# Check the installed Bun version
bun --version

Updating and auditing dependencies#

Before transitioning, use Node's package manager to map obsolete packages that could generate conflicts in the new runtime:

# List outdated dependencies in the Node.js environment
npm outdated

Post-migration to Bun, to update the mapped dependencies in the Bun lockfile, run:

# Update project dependencies under Bun's manager
bun update
ActionNode.js Command (NPM)Bun Command
Install Dependenciesnpm installbun install
Add Packagenpm install <package>bun add <package>
Remove Packagenpm uninstall <package>bun remove <package>
Run Scriptnpm run <script>bun run <script>
Run Local Binarynpx <command>bunx <command>

2. Candidate service selection and dependency auditing#

Do not begin the migration with critical monolithic services processing payments, authentication, or sensitive data flows. The ideal approach is to select simple microservices, batch queue consumers, or stateless APIs that have high test coverage and container-isolated deployment.

Auditing native dependencies (node-gyp)#

The biggest compatibility obstacle comes from libraries that rely on node-gyp to compile native C/C++ code during installation. Although Bun provides a compatibility layer for Node's C++ API (N-API), complex dependencies often fail during the build process or crash silently at runtime.

Use jq to parse your package.json and identify critical native dependencies:

# Scan dependencies that commonly use native compilation
cat package.json | jq '.dependencies | to_entries[] | select(.key | contains("native") or contains("gyp") or contains("sqlite3") or contains("bcrypt") or contains("canvas") or contains("sharp"))'

Alternatively, run a recursive search in the node_modules directory to locate files and references to the node-gyp compiler:

# Identify packages that use node-gyp to build native bindings
grep -r "node-gyp" package.json node_modules/*/package.json 2>/dev/null

Auditing lockfiles#

Bun uses a high-performance binary format for its lockfile (bun.lockb). Verify the presence of the project's control files:

# Inspect the dependency lockfiles in the repository
ls -la bun.lockb package-lock.json yarn.lock 2>/dev/null

If you are migrating a project, bun install will read the existing package-lock.json or yarn.lock to replicate the exact tree of versions, then generate bun.lockb.


3. Typescript support without external transpilation#

Unlike Node.js, which requires tools like ts-node, tsx, or a separate transpilation step via tsc/esbuild before running code in production, Bun executes .ts and .tsx files directly. It has a native transpiler integrated into the JavaScriptCore engine that parses TypeScript syntax and executes it transparently, with no noticeable startup overhead.

Checking tsconfig.json#

Although Bun ignores most compilation configurations in tsconfig.json (as it executes code directly without generating intermediate files), path resolution (paths) and import directives must be aligned. Validate the configuration file:

# Check if the TypeScript configuration file exists
ls -la tsconfig.json 2>/dev/null

Type validation and build#

Because Bun does not perform static type checking at runtime (it only strips type annotations to execute JavaScript), typing errors can go unnoticed. To ensure code integrity at a static level, use the official TypeScript compiler for validation without emitting code:

# Run static TypeScript type validation without generating files
npx tsc --noEmit

To test the final optimized project packaging or build defined in your package.json scripts, run:

# Test the project's build routine under Bun
bun run build

4. Monitoring and benchmarks in real environments#

To avoid the self-deception of artificial laboratory benchmarks, performance comparisons must occur on the same physical host or container under strict CPU and memory limits.

Load testing with autocannon#

Use autocannon to fire controlled load at your application and compare fundamental metrics such as throughput (requests per second) and response latency:

# Run a 30-second load test with 100 simultaneous connections
npx autocannon -c 100 -d 30 http://localhost:3000/health

To extract only the p99 latency for SRE reporting, process the output with jq:

# Filter the report to display p99 percentile latency in milliseconds
npx autocannon -c 100 -d 30 http://localhost:3000/health --json | jq '.latency.p99'

System resource monitoring#

During the benchmark run under load, capture the real resident set size (RSS) memory and CPU consumption of the active Bun process in real time:

# Monitor PID, RSS (in KB), VSZ, and CPU usage of the active Bun process
ps -p $(pgrep -f "bun run") -o pid,rss,vsz,%cpu,cmd

HTTP return code validation#

Ensure that the migration is not generating silent HTTP errors (5xx or sudden crashes). Test the health check endpoint:

# Verify if the returned HTTP status code is exactly 200 OK
curl -s http://localhost:3000/health -w "%{http_code}" -o /dev/null

Error log auditing#

Actively monitor system logs for uncaught exceptions, segmentation faults, or runtime panics:

# Monitor logs in real time for crashes or critical runtime failures
tail -f /var/log/app/error.log | grep -iE "error|crash|panic|segfault"

Typical performance comparison table#

MetricNode.js BaselineBun CandidateVerdict
Throughput (req/sec)12,50028,900Bun (+131%)
p95 Latency18 ms6 msBun (3x faster)
p99 Latency42 ms11 msBun (Better stability)
Memory Usage (Startup)78 MB31 MBBun (-60%)
Memory Usage (Under Load)180 MB92 MBBun (-48%)
Cold Start Time180 ms15 msBun (Better for Serverless)
HTTP 5xx Error Rate0.00%0.00%Equivalent

5. Advanced environment variable management#

Bun eliminates the need to load external libraries like dotenv or dotenv-expand to read .env files. The runtime automatically parses these files at application bootstrap.

By default, Bun searches for and injects variables from the following files in descending order of precedence:

  1. .env.local
  2. .env.production or .env.development (based on NODE_ENV)
  3. .env

To explicitly force loading a specific .env file, use the --env-file flag:

# Run the application forcing the injection of variables from a custom file
bun run --env-file=.env.production src/server.ts

Alternatively, when using scripts defined in package.json, you can append the flag directly to the executable:

# Run start script injecting custom environment variables
bun --env-file=.env run start

In JavaScript/TypeScript code, retrieving variables is backward-compatible with Node.js (process.env.VARIAVEL), but Bun also offers the native and faster shortcut import.meta.env.VARIAVEL.


6. Native WebSockets: high performance vs. node libraries#

In the Node.js ecosystem, implementing high-performance WebSockets requires using external dependencies such as ws or socket.io. In Bun, the WebSocket protocol is supported natively in the HTTP server engine via Bun.serve(), implemented directly in C++ on top of the uWebSockets library.

Code scan for WebSocket dependencies#

To identify if the current project uses external WebSocket libraries, run:

# Search for WebSocket library usage in code and configuration
grep -r "WebSocket\|socket.io\|ws" package.json src/

Practical WebSocket server example with Bun#

Below is a working example of a server handling WebSocket connections natively and optimally:

// src/websocket-server.ts
Bun.serve({
  port: 3000,
  fetch(req, server) {
    // Upgrade regular HTTP request to a WebSocket connection
    const success = server.upgrade(req);
    if (success) {
      return undefined; // Upgrade successful
    }
    return new Response("Upgrade failed", { status: 400 });
  },
  websocket: {
    open(ws) {
      console.log(`Client connected: ${ws.remoteAddress}`);
      ws.subscribe("global-channel");
    },
    message(ws, message) {
      console.log(`Message received: ${message}`);
      // Echo message to all subscribers of the channel
      ws.publish("global-channel", `Echo: ${message}`);
    },
    close(ws, code, message) {
      console.log(`Client disconnected. Code: ${code}`);
      ws.unsubscribe("global-channel");
    }
  }
});
console.log("Native WebSocket server running on port 3000");

Testing the WebSocket connection#

Use the wscat utility to interactively validate the WebSocket server's operation:

# Connect to the local WebSocket server to test messaging
npx wscat -c ws://localhost:3000/ws

7. Native http/2 support#

HTTP/2 support in Bun is native and integrated directly into the Bun.serve API. When configuring TLS security keys (SSL certificates), the server automatically negotiates HTTP/2 connections via ALPN without the need for complex third-party modules like http2 or spdy, which are common in Node.js.

Auditing legacy http/2 modules#

Identify if the legacy Node.js application depends on obsolete or specific HTTP/2 libraries:

# Search for references to http2/spdy packages or modules
grep -r "http2\|spdy" package.json src/

Validating http/2 in production#

After starting the service with Bun, validate if the handshake and HTTP/2 multiplexing are active using curl:

# Inspect HTTP response headers validating the HTTP/2 protocol
curl --http2 -I https://localhost:3000/

8. Regression testing and code coverage#

Bun includes a built-in and extremely fast test runner (bun test) compatible with major Jest and Vitest APIs. This eliminates the need to install and configure complex JavaScript testing frameworks.

Running tests#

To run the entire project test suite using Bun:

# Execute all matching test files (*.test.ts, *.spec.js, etc.)
bun test

Filtering tests#

If you want to run only a specific suite or tests based on name patterns, use the --filter flag:

# Run only tests matching the string "auth"
bun test --filter "auth"

Code coverage reporting#

To audit code coverage natively and without the need for tools like Istanbul/c8:

# Run tests generating coverage report directly in the terminal
bun test --coverage

Comparison with Node.js test runner#

Node.js 18+ also introduced a native test runner (node --test), but it does not feature built-in assertions (requiring imports of the node:assert module) or native TypeScript transpilation.

# Run native tests in Node.js (requires prior transpilation for TypeScript)
node --test

9. Complete CI/CD integration (GitHub actions)#

Runtime migration must be validated on every commit and pull request to prevent behavioral regressions or build failures. The example below shows a complete and optimized pipeline for GitHub Actions, using the official Bun action.

Create or update the .github/workflows/migrate-to-bun.yml file:

name: CI/CD Pipeline - Bun Validation

on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]

jobs:
  validate-and-build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Code
        uses: actions/checkout@v4

      - name: Set Up Bun Ecosystem
        uses: oven-sh/setup-bun@v1
        with:
          bun-version: latest # Or lock to an exact stable version, e.g. 1.1.0

      - name: Install Dependencies
        run: bun install --frozen-lockfile

      - name: Static Type Validation (TypeScript)
        run: npx tsc --noEmit

      - name: Execute Test Suite
        run: bun test --coverage

      - name: Production Compiling
        run: bun run build

10. Dependency security and auditing#

Bun's package manager features an extremely fast, native security auditing utility to map known vulnerabilities in the dependency tree (CVEs).

Vulnerability auditing#

To run a security scan against the official npm database under Bun:

# Audit dependencies for known security flaws
bun audit

If you need to compare or run classically with Node.js npm:

# Run vulnerability audit via classic NPM
npm audit

Auditing licenses and suspicious packages#

To list all installed dependencies, including indirect ones, facilitating the identification of malicious packages (malware), typo-squating, or restrictive licenses incompatible with your business guidelines:

# List all dependencies in the tree with detailed info
bun pm ls --all

You can filter the output to validate licenses or suspicious dependencies:

# Filter installed dependencies for license terms
bun pm ls --all | grep -i "license"

Rollout, rollback, and hardened dockerfile strategy#

To ensure a safe transition, deployment must be done in isolated containers using security best practices (non-root user and locked dependencies).

Optimized production dockerfile (Bun)#

This multi-stage Dockerfile ensures a lightweight image with optimized caching running under the restricted system user bun:

# Stage 1: Installing Dependencies
FROM oven/bun:1.1-alpine AS base
WORKDIR /usr/src/app

# Copy dependency files
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile

# Stage 2: Building Application
FROM base AS builder
WORKDIR /usr/src/app
COPY . .
RUN bun run build

# Stage 3: Running Production Image (No root privileges)
FROM oven/bun:1.1-alpine AS release
WORKDIR /usr/src/app

# Copy only necessary artifacts
COPY --from=builder /usr/src/app/package.json ./
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/src ./src

# Expose service port
EXPOSE 3000

# Run with least-privilege user
USER bun
ENTRYPOINT ["bun", "run", "src/server.ts"]

Docker compose configuration with rollback strategy#

To allow immediate fallback to Node.js in case of runtime anomalies detected in Bun in production, configure your docker-compose.yml keeping the Node container inactive but ready to be promoted instantly via reverse proxy or DNS:

version: "3.8"

services:
  # Primary Service running in Bun
  app-bun:
    build:
      context: .
      dockerfile: Dockerfile
    image: my-app:bun-latest
    container_name: app-prod-bun
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      - NODE_ENV=production
    restart: always
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 10s
      timeout: 5s
      retries: 3

  # Secondary Rollback Service running in Node.js
  app-node:
    image: node:18-alpine
    container_name: app-prod-node
    volumes:
      - .:/app
    working_dir: /app
    command: ["node", "src/server.js"]
    ports:
      - "127.0.0.1:3001:3000"
    environment:
      - NODE_ENV=production
    restart: no
    profiles:
      - rollback

If Bun's latency or error rate SLO degrades, execute the immediate rollback command without compiling code during the crisis:

# Stop Bun container and spin up the Node backup image
docker compose stop app-bun && docker compose --profile rollback up -d app-node

Operational migration checklist#

Strictly follow the steps below before, during, and after promoting Bun to production:

Phase 1: Pre-migration#

Phase 2: Installation and local testing#

Phase 3: Benchmark and load validation#

Phase 4: CI/CD and infrastructure integration#

Phase 5: Rollout and post-deploy#

Was this article helpful?

Leave a quick reaction to help prioritize future technical guides:

CC BY-NC

This post is licensed under CC BY-NC.

Comments

Join the discussion below.

0 comments