Tools

Bun: The Fast JavaScript Runtime

Mayur Dabhi
Mayur Dabhi
June 11, 2026
14 min read

You clone a repo, run npm install, and go make coffee. Then CI does the same thing on every push. Then you add ts-node to run one TypeScript script, Jest plus ts-jest to test it, and a bundler config to ship it. None of these tools is slow on its own, but together they add a lot of waiting and a lot of config files.

Bun tries to fold all of that into one binary: a runtime, package manager, bundler, and test runner. It's written in Zig and runs on JavaScriptCore instead of V8. Below I'll cover how to install it, what each piece does, where it's actually faster, and the cases where I'd still stick with Node.js.

Why Is Bun Fast?

Bun uses Apple's JavaScriptCore engine (the one in Safari) instead of V8, and JavaScriptCore tends to start up faster. Bun itself is written in Zig, a systems language with manual memory management, so the tooling around your code (the installer, transpiler, and HTTP server) runs as native code instead of JavaScript. Your own code is still garbage collected like it would be anywhere else.

What You Get in One Binary

Install Bun and you get four tools that you'd normally pull in separately:

Jarred Sumner started the project, and v1.0 shipped in September 2023. It's open source under the MIT license.

bun Single Binary (Zig) JavaScriptCore Apple's JS Engine Runtime JS & TS execution Web APIs built-in Package Manager bun install / add npm compatible Bundler ESM / CJS / CSS Tree shaking Test Runner Jest-compatible API bun test Bun Architecture: One Binary, Four Tools

Bun bundles runtime, package manager, bundler, and test runner into a single binary

Installation and Setup

Installing Bun is one command on macOS, Linux, or WSL. The install script drops a single binary in ~/.bun/bin/bun and adds it to your shell profile, so open a new terminal (or source your profile) before running bun.

1

Install Bun

Run the official install script. It works on macOS, Linux, and Windows via WSL.

Terminal: Install Bun
# macOS / Linux / WSL (recommended)
curl -fsSL https://bun.sh/install | bash

# macOS with Homebrew
brew install oven-sh/bun/bun

# npm (if you already have Node.js)
npm install -g bun

# Verify installation
bun --version
# e.g. 1.1.34
2

Create a new project

Use bun init to scaffold a new project interactively, or bun create to use a template.

Terminal: Create a project
# Interactive project init
bun init

# Create from a template
bun create react my-app       # React + Vite
bun create next my-app        # Next.js
bun create hono my-api        # Hono web framework
bun create elysia my-api      # Elysia (Bun-first framework)

# Change directory and run
cd my-app
bun run dev
3

Run any JavaScript or TypeScript file

Bun runs .js, .ts, .jsx, and .tsx files natively. You don't need tsc, Babel, or ts-node.

Terminal: Running files
# Run a TypeScript file directly
bun run index.ts

# Watch mode (re-run on file changes)
bun --watch run index.ts

# Execute a one-liner
bun -e "console.log(Bun.version)"
Windows Support

Native Windows support (without WSL) became stable in Bun v1.1. You can install it with powershell -c "irm bun.sh/install.ps1 | iex" on Windows 10/11.

Bun as a Package Manager

This is the part you can adopt today without changing your runtime. bun install reads your existing package.json and pulls from the npm registry, but it keeps a global cache and links packages from it, so reinstalls on a warm machine are very quick.

Package Manager Commands

# Install all dependencies from package.json
bun install

# Install without devDependencies (production)
bun install --production

# Install frozen (fails if bun.lockb would change)
bun install --frozen-lockfile

# Force re-download (ignore cache)
bun install --force
# Add a dependency
bun add express
bun add zod

# Add a dev dependency
bun add -d typescript @types/express

# Add a peer dependency
bun add --peer react

# Remove a package
bun remove lodash

# Upgrade all packages
bun update
# Run a script from package.json
bun run build
bun run test
bun run dev

# Shorthand: only safe when the name isn't a built-in command
bun dev
bun build

# Run a local binary from node_modules
bun x tsc --version          # like npx but faster
bunx eslint src/              # alias for bun x
# package.json (root of a monorepo)
{
  "name": "my-monorepo",
  "workspaces": ["packages/*", "apps/*"]
}

# Install all workspace dependencies at once
bun install

# Run a script in a specific workspace
bun run --filter="@myapp/ui" build

# Run a script in all workspaces
bun run --filter="*" test
Gotcha: bun build and bun test

Built-in commands win over package.json scripts. If you have a "build" script that runs Vite, bun build ignores it and starts Bun's bundler instead, usually failing with a "Missing entrypoints" error. Same with bun test: it runs Bun's test runner, not your "test" script. Use bun run build and bun run test to be explicit.

Lockfile: bun.lockb

Older Bun versions write a binary lockfile (bun.lockb) instead of a text file like package-lock.json. Commit it like any other lockfile. The downside is that diffs in pull requests are unreadable, which is why Bun 1.2 switched the default to a text bun.lock. If you're still on the binary format, you can print it as text:

Terminal
# Print the binary lockfile as text (Yarn v1 lockfile format)
bun bun.lockb

Building HTTP Servers with Bun

Bun.serve() is built on the standard Request and Response objects, so if you've written a Cloudflare Worker or a Next.js route handler, it'll look familiar. It handles noticeably more requests per second than Node's http module in hello-world benchmarks, and WebSockets are built in. A minimal REST API:

server.ts: HTTP server with Bun.serve()
const PORT = process.env.PORT ? Number(process.env.PORT) : 3000;

const server = Bun.serve({
  port: PORT,
  async fetch(req) {
    const url = new URL(req.url);

    // GET /
    if (url.pathname === "/" && req.method === "GET") {
      return Response.json({ message: "Hello from Bun!", version: Bun.version });
    }

    // GET /users
    if (url.pathname === "/users" && req.method === "GET") {
      const users = [
        { id: 1, name: "Alice" },
        { id: 2, name: "Bob" },
      ];
      return Response.json(users);
    }

    // POST /users
    if (url.pathname === "/users" && req.method === "POST") {
      const body = await req.json();
      // ... save to database
      return Response.json({ created: true, user: body }, { status: 201 });
    }

    return new Response("Not Found", { status: 404 });
  },
});

console.log(`Listening on http://localhost:${server.port}`);
Terminal
bun run server.ts
# Listening on http://localhost:3000

WebSocket Support

You don't need the ws package. Add a websocket handler next to fetch and call server.upgrade():

ws-server.ts: WebSockets with Bun.serve()
Bun.serve({
  port: 3000,
  fetch(req, server) {
    // Upgrade HTTP request to WebSocket
    if (server.upgrade(req)) return; // upgrade succeeded
    return new Response("Use WebSocket", { status: 426 });
  },
  websocket: {
    open(ws) {
      console.log("Client connected");
      ws.send("Welcome!");
    },
    message(ws, data) {
      console.log("Received:", data);
      ws.send(`Echo: ${data}`);
    },
    close(ws) {
      console.log("Client disconnected");
    },
  },
});

Bun's Built-in Test Runner

The test runner copies Jest's API on purpose: describe, it, expect, beforeEach, afterAll, and mocks all look the same. Most Jest suites move over with few or no changes, and TypeScript test files run without ts-jest.

math.test.ts: Writing tests with Bun
import { describe, it, expect, beforeEach, mock } from "bun:test";

// Simple utility being tested
function add(a: number, b: number) {
  return a + b;
}

describe("add()", () => {
  it("adds two positive numbers", () => {
    expect(add(2, 3)).toBe(5);
  });

  it("handles negative numbers", () => {
    expect(add(-1, 1)).toBe(0);
  });

  it("returns the correct type", () => {
    expect(typeof add(1, 2)).toBe("number");
  });
});

// Mocking
const fetchData = mock(async (id: number) => {
  return { id, name: "Mocked User" };
});

describe("fetchData()", () => {
  it("calls the mock once", async () => {
    const result = await fetchData(1);
    expect(fetchData).toHaveBeenCalledTimes(1);
    expect(result.name).toBe("Mocked User");
  });
});
Terminal: Running tests
# Run all test files
bun test

# Watch mode
bun test --watch

# Run a specific file
bun test math.test.ts

# Run tests matching a pattern
bun test --test-name-pattern "adds two"

# Code coverage
bun test --coverage
Migrating from Jest

Change import { describe, it, expect } from "@jest/globals" or jest to "bun:test" and remove the jest and ts-jest dependencies. Most suites work after that. The ones that don't usually lean on Jest-specific features like a custom testEnvironment (jsdom) or less common matchers, so run the full suite once before deleting Jest.

Bun's Bundler

bun build bundles JavaScript and TypeScript for browsers, Bun, or Node. It handles ESM, CommonJS, CSS, and static assets. It doesn't have the plugin ecosystem of Webpack or Rollup, so I wouldn't move a heavily customized frontend build to it. For server code, small libraries, and quick build steps, it's fast and needs almost no config.

Terminal: Bundling a project
# Bundle for browser (default target)
bun build ./src/index.ts --outdir ./dist

# Minified production build
bun build ./src/index.ts --outdir ./dist --minify

# Bundle as CommonJS module
bun build ./src/lib.ts --outdir ./dist --format cjs

# Bundle with sourcemaps
bun build ./src/index.ts --outdir ./dist --sourcemap external

# Bundle everything into a single file
bun build ./src/index.ts --outfile ./dist/bundle.js

# Target specific environments
bun build ./src/index.ts --target browser   # Browser (default)
bun build ./src/index.ts --target bun       # Bun runtime
bun build ./src/index.ts --target node      # Node.js

Using the Bundler API

You can also call the bundler from a build script, which is handy once the CLI flags get long:

build.ts: Programmatic bundling
const result = await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  minify: true,
  sourcemap: "external",
  target: "browser",
  define: {
    "process.env.NODE_ENV": '"production"',
  },
  plugins: [
    // Bun has a plugin API compatible with esbuild plugins
  ],
});

if (!result.success) {
  console.error("Build failed:", result.logs);
  process.exit(1);
}

console.log(`Built ${result.outputs.length} files`);

TypeScript and JSX Without Config

You don't need to install typescript, set up tsconfig.json for execution, or run tsc first. Bun's own transpiler strips the types when it loads the file.

Type Checking

Bun executes TypeScript by stripping types. It does not type-check your code, so a file full of type errors will still run. For type checking, still run bun x tsc --noEmit in your CI pipeline. Think of Bun as a replacement for ts-node, not for the TypeScript compiler.

app.tsx: JSX with Bun (React)
// Bun reads jsxImportSource from tsconfig.json automatically
// No Babel, no extra config needed

import { renderToString } from "react-dom/server";

function App() {
  return (
    <html>
      <head><title>My App</title></head>
      <body>
        <h1>Hello from Bun + React SSR!</h1>
      </body>
    </html>
  );
}

const html = renderToString(<App />);

Bun.serve({
  port: 3000,
  fetch() {
    return new Response(html, {
      headers: { "Content-Type": "text/html" },
    });
  },
});

Bun vs Node.js: Performance Comparison

Treat the numbers below as rough ballpark figures, roughly in line with what Bun publishes and what community benchmarks report. They depend heavily on hardware, project size, and network. The install and startup gains show up in almost any project. The requests-per-second gap is from hello-world servers. Once your handler waits on a database, that gap shrinks a lot.

Benchmark Node.js Bun Speedup
bun install vs npm install (cold) ~8s ~0.7s ~11×
bun install vs npm install (cached) ~2s ~0.1s ~20×
Script startup time (hello.ts) ~120ms ~10ms ~12×
HTTP requests/sec (hello world) ~80k req/s ~250k req/s ~3×
Running a test suite (100 tests) ~3.2s (Jest) ~0.4s (bun test) ~8×
Bundling a medium SPA ~4s (Webpack) ~0.3s (bun build) ~13×
Package Install Time (smaller = faster) npm 8.0s yarn 5.2s pnpm 3.7s bun 0.7s

Illustrative cold install times for a project with ~200 packages (your numbers will differ)

Node.js Compatibility

Bun aims to be fully compatible with Node.js, and most npm packages and Node code run unchanged. What it implements:

Bun's Built-in Globals (No Imports Needed)

Global Description
Bun.file(path) Open a file as a lazy blob (nothing is read until you ask)
Bun.write(path, data) Write a string, Buffer, or blob to disk
Bun.serve(options) Start an HTTP/WebSocket server
Bun.build(options) Bundle files programmatically
Bun.hash(data) Fast non-cryptographic hash (Wyhash)
Bun.password.hash(pw) Hash a password with Argon2 or bcrypt
Bun.sql Built-in PostgreSQL client (Bun v1.2+)
bun:sqlite Built-in SQLite driver (a module import, not a global)
Bun.spawn() Run child processes with streaming I/O
Bun.env Alias for process.env with .env auto-loaded

Built-in SQLite With No Dependencies

For small backends, scripts, and local tooling, bun:sqlite saves you a native dependency. It's a synchronous SQLite client with an API modeled on better-sqlite3, and there's nothing to install or compile:

db.ts: Built-in SQLite with Bun
import { Database } from "bun:sqlite";

// Open or create a database file
const db = new Database("myapp.db");

// Create a table
db.run(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )
`);

// Prepared statement (reusable, prevents SQL injection)
const insertUser = db.prepare(
  "INSERT INTO users (name, email) VALUES ($name, $email)"
);

// Insert a user
insertUser.run({ $name: "Alice", $email: "alice@example.com" });
insertUser.run({ $name: "Bob",   $email: "bob@example.com" });

// Query all users
const allUsers = db.query("SELECT * FROM users").all();
console.log(allUsers);
// [{ id: 1, name: 'Alice', email: 'alice@example.com', ... }, ...]

// Query a single user
const getById = db.prepare("SELECT * FROM users WHERE id = $id");
const user = getById.get({ $id: 1 });
console.log(user?.name); // "Alice"

db.close();

When to Use Bun vs Node.js

Bun can't replace Node.js everywhere yet. Some native addons (.node files), some Webpack plugins, and a handful of npm packages that reach into Node internals can still break. This is roughly how I'd decide:

Scenario Recommended Reason
New greenfield project Bun Native TS and a built-in test runner, so less setup from day one
CLI tools and scripts Bun Much faster startup, and TS files run without a build step
High-throughput API server Bun Bun.serve() handles more req/s with less memory
Monorepo with npm workspaces Bun Much faster installs in CI
Project with native addons (.node) Node.js Bun implements most of Node-API, but addon support is the most likely thing to break
Legacy Express + complex middleware stack Node.js Some edge cases in middleware that rely on Node internals
Next.js / Nuxt (full-stack framework) Both Use Bun as package manager; runtime support improves each release

How I'd Adopt Bun (and Where I'd Hold Off)

You don't have to switch everything at once, and I wouldn't. Bun's pieces are separate enough that you can take the low-risk ones first and stop wherever things get uncomfortable.

A low-risk order to try it

  • Package manager first: Run bun install in CI and keep Node as the runtime. If something breaks, the fix is reverting one line in your pipeline.
  • Then scripts: Seed scripts, codegen, and one-off TypeScript tools are where fast startup and no build step pay off most.
  • Then tests: Try bun test on a branch. If your suite depends on jsdom or heavy Jest config, expect some work.
  • Runtime last: Only move a production server once it passes your full test suite on Bun and you've load-tested it with real database calls, not hello-world.

Where I'd hold off: anything that depends on native addons, apps whose hosting platform only officially supports Node, and teams where nobody has time to chase down the occasional compatibility bug. Node is boring in a good way, with long-term support releases and a huge pool of people who've already hit your error message. Bun moves fast, which is great for features but means you should pin its version (in CI and in your Dockerfile) instead of pulling latest.

If you just want to see whether it's worth it, run bun install on a project you already have and time it against your current package manager. That takes five minutes and tells you more than any benchmark table.

Bun JavaScript Runtime Performance TypeScript Package Manager Node.js
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications with Laravel, React, and Node.js.