Bun, one binary instead of four tools
Bun replaces four things at once: it runs code like Node, installs dependencies like npm, bundles like esbuild, and executes tests like Jest. All of it sits in a single binary written in Zig and built on JavaScriptCore, the same engine that powers Safari.
What changes in daily work
A typical JavaScript project today carries four tools from four teams, each with its own configuration and release cycle. Bun folds them into one, so the configuration layer holding them together disappears.
The most noticeable change concerns TypeScript. Bun executes .ts and .tsx files directly, with no compilation step and no extra tool in the middle.
bun run script.tsThe same file under Node needs either prior compilation or a tool intercepting imports. In practice that means faster startup and fewer things that break when a version changes.
The second change is dependency installation speed. The gap against traditional package managers runs several times over, which shows most in continuous integration where installation repeats on every run.
A package manager compatible with the ecosystem
Bun reads package.json and installs from the npm registry, so it demands no change to how dependencies are described.
bun install
bun add zod
bun add -d vitest
bun remove lodashOne thing is worth knowing for team work. Bun writes its own lockfile, and its default format has been binary, which makes a pull request diff say nothing. If the rest of the team uses a different manager, agree on one, because two parallel lockfiles lead to everybody holding a different library version.
Global tool installation works too, and bunx runs a package without installing it permanently, mirroring the command familiar from npm.
A built in test runner
You run tests without adding a library, and the syntax matches what most teams already know.
import { test, expect, describe } from "bun:test"
describe("discount calculator", () => {
test("applies the threshold discount", () => {
expect(calculateDiscount(1200)).toBe(120)
})
test("applies nothing below the threshold", () => {
expect(calculateDiscount(300)).toBe(0)
})
})bun test
bun test --watch
bun test --coverageMigrating from Jest, most unit tests pass unchanged. Trouble starts with tests built on elaborate module mocks and with libraries assuming a specific environment, so before deciding, run your current suite and see what breaks.
HTTP server and a built in database
Bun ships its own server built on the standard Request and Response objects, the same ones you know from the browser and from API routes in Next.js.
Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url)
if (url.pathname === "/health") return new Response("ok")
return Response.json({ error: "not found" }, { status: 404 })
}
})On top of that comes a SQLite client built into the runtime, with no native module to install.
import { Database } from "bun:sqlite"
const db = new Database("data.sqlite")
db.run("CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY, name TEXT)")
db.query("INSERT INTO events (name) VALUES (?)").run("start")A file database carries one limitation worth naming right away: it sits on that machine's disk, so a second instance of the service sees its own copy, and deploying across several machines stops making sense. Once that starts to hurt, Turso offers the same SQLite syntax as a service, with a replica synced into the application itself, at the cost of reads showing state from a moment ago.
Beyond SQLite, the runtime also ships clients for popular databases and caches, so a typical service does without several dependencies people install by reflex. Fewer dependencies means fewer security updates to track, which on small projects often matters more than raw speed.
That arrangement suits small services and internal tools well: one binary, one database in a file, nothing to install on the server.
The bundler and building a package
The fourth tool in the set is a bundler. It builds both browser code and a single server file ready to deploy.
bun build ./src/index.ts --outdir ./dist --target node --minify
bun build ./src/app.tsx --outdir ./public --target browser --splittingThe target parameter decides how built in modules are treated. The browser build strips filesystem references, the server build keeps them, and the Bun specific variant uses its own interfaces.
In practice the bundler suits libraries, command line tools, and small applications well. For an elaborate frontend with many plugins, the ecosystem around more mature bundlers remains richer, so check that the plugins you need have equivalents before rewriting your build configuration.
Separately, it is worth knowing the option to build a single executable containing both code and runtime.
bun build ./cli.ts --compile --outfile toolThe result runs without installing anything. For tools distributed inside a company that solves a problem which otherwise ends in instructions about installing the right Node version. The price is file size measured in tens of megabytes, since the runtime comes along.
Scripts instead of helper tooling
Bun works well as a replacement for the helper scripts that in many projects are written in shell and understood by nobody six months later.
// scripts/cleanup.ts
import { readdir, rm } from "node:fs/promises"
const directories = await readdir("./tmp")
for (const directory of directories) {
if (directory.startsWith("build-")) {
await rm(`./tmp/${directory}`, { recursive: true })
console.log("removed", directory)
}
}You run it with bun scripts/cleanup.ts, no compilation and no dependencies added. Typed code is easier to maintain than a shell script, and differences between operating systems stop being a problem.
The built in interfaces cover most needs of such scripts: reading and writing files, network calls, spawning subprocesses, access to a file database. For recurring jobs, a report generated nightly for instance, everything fits into one file and one scheduler entry.
Just mind error handling. A script exiting quietly after a failed network call is worse than no script, because it creates the impression the job ran. Set a non zero exit code on failure so the scheduler or pipeline notices.
Node compatibility and its real limits
Compatibility grows with every release and most popular libraries work unchanged. Problems appear in three places worth checking before committing to a migration.
Native modules compiled against Node cause the most trouble, especially older database drivers and image processing libraries. Rarely used parts of the standard API sometimes behave differently in edge cases. Tools assuming a particular environment, some build plugins for instance, may need a workaround.
A practical check takes fifteen minutes: install dependencies with Bun and run the whole test suite. If it passes, migration risk is low. If it fails on a native module, you know before rewriting any deployment configuration.
Releases in the 1.3 line are stable and used in production, and each one closes more of the gap against Node. Before upgrading a production project, read the change list, since development moves noticeably faster here than in mature runtimes.
Where the speed difference comes from
It helps to understand where the advantage lies, because not every part of a project feels it equally.
The first source is the engine. Bun uses JavaScriptCore rather than V8, and it starts faster with modest memory overhead. For short lived processes such as scripts and on demand functions, startup time alone can be a noticeable share of the total.
The second is how the package manager works. Installation uses parallel file operations and a locally held cache, so reinstalling the same dependencies runs many times faster than the first pass. That is why the largest difference shows in continuous integration, where this step repeats endlessly.
The third is the absence of intermediate layers. Running a TypeScript file needs no separate compiler process, and the test runner loads no module environment of its own.
What the advantage does not cover: executing application code over a long run. Once a process runs for hours, the engine's compiler optimises the hot paths anyway and the gap between engines flattens. If your service is slow because of database queries or network calls, changing the runtime fixes nothing.
The fairest way to judge is therefore measurement on your own project: dependency install time, test suite duration, and process startup time. Those three numbers usually settle the decision, while synthetic comparisons rarely translate into daily work.
Bun against Node and Deno
| Tool | Strength | Weakness | Pick it when |
|---|---|---|---|
| Bun | Everything in one, fast installs, TypeScript without compilation | Younger ecosystem, occasional incompatibilities | New project, internal tools, scripts |
| Node | Broadest compatibility, supported by every host | Separate tools for building and testing | Production project with many dependencies |
| Deno | Permissions restricted by default, built in tooling | Different dependency model in older versions | Scripts needing access control |
Practical advice for a team: the easiest way in is from the back, adopting Bun as the package manager and test runner while production stays on Node. You gain time in continuous integration with no production risk, and after a few weeks you know whether to go further.
Deployment and hosting
Not every host supports Bun natively, so check before choosing. A container image settles the matter anywhere you control the runtime environment.
FROM oven/bun:1
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
CMD ["bun", "run", "start"]The flag enforcing lockfile fidelity matters here: without it a build can install versions other than the ones you tested locally.
A separate option is building a single executable containing both code and runtime. For command line tools distributed inside a company that is convenient, since the recipient installs nothing.
When deploying to platforms such as Vercel, check whether the function runs in an environment supporting Bun or whether only dependency installation uses it during the build. Those are two different things and easy to confuse.
Introducing Bun into an existing team
Changing a tool in a project several people work on rarely fails on technical grounds. It fails because half the team has a differently configured environment and nobody wants to spend a day diagnosing somebody else's error.
An order that works looks like this. First one person runs the test suite under Bun and records what breaks. If nothing does, you move to step two, swapping the package manager in continuous integration but not yet on developer machines. That step reverses with one line of configuration and shows the time saving immediately.
Step three is local environments, together with deleting the old lockfile in the same commit. Two lockfiles in a repository are the most common source of trouble in such a migration, so the switch has to be a single move rather than a gradual one.
Change the runtime last and separately, ideally starting with the lowest risk service. An internal application or a background job suits that better than the main customer facing service.
Write down in the project documentation which version you use and how to install it. The tool moves fast, and a few releases apart can change behaviour nobody tested.
Common mistakes
The first is migrating the whole project at once, production included. Starting with dependency installation and tests, and changing the runtime separately, is wiser.
The second is two lockfiles in one repository. If part of the team uses npm and part uses Bun, dependency versions drift silently until the first bug that reproduces on one machine only.
The third is omitting the lockfile fidelity flag in the pipeline. A build should install exactly what was tested, not the newest matching versions.
The fourth is assuming full compatibility without checking. Running the test suite under Bun answers that question in a quarter of an hour.
The fifth is using the built in SQLite as a production database for an application with multiple instances. A file on disk does not share between processes the way a horizontally scaled application expects.
FAQ
Will Bun replace Node.js?
For new projects and internal tooling it is often more convenient, while Node remains the safer choice wherever ecosystem compatibility and universal hosting support matter. Many teams use both: Bun for installs and tests, Node in production.
Does Bun work with Next.js and React?
Installing dependencies and running scripts works without obstacle. For running the application server itself, check compatibility with your framework version, since some parts assume a Node environment.
Do I have to change package.json?
No, Bun reads the same file and installs from the npm registry. The lockfile changes, so agree with your team on one manager rather than maintaining two.
Will Jest tests pass unchanged?
Most unit tests will, since the syntax matches. Elaborate module mocks and environment dependent tests usually need adjustment. The fastest check is running your current suite with bun test.
Is Bun production ready?
Yes, the 1.3 line runs in production, though pinning a specific version and following the change list is wise. Release cadence is higher than in mature runtimes, so upgrading without checking invites surprises.
Documentation sits at bun.com/docs, and the source code in the GitHub repository.