Bun vs Deno 2 vs Node 22: JavaScript Runtimes in 2026
Bun vs Deno 2 vs Node 22: JavaScript Runtimes in 2026
TL;DR
Node.js 22 LTS remains the production default for 99% of teams — unmatched ecosystem compatibility, battle-tested stability, and the largest set of production examples. Bun 1.x is the speed king — 2–4x faster startup, faster package installs, and a compelling all-in-one toolchain (runtime + bundler + test runner + package manager). Deno 2 finally fixed its npm compatibility story and now runs most Node.js code, while adding top-tier security sandboxing, built-in TypeScript, and native Web APIs. Pick Bun when performance matters, Deno when you want security-by-default, Node when you need the deepest ecosystem.
Key Takeaways
- Bun installs npm packages 10–25x faster than npm and 3–5x faster than pnpm, consistently
- Node.js 22 added experimental TypeScript stripping —
node --experimental-strip-types index.tsruns TS without transpilation - Deno 2 is now npm-compatible —
deno run npm:expressjust works. The 2024 ecosystem pivot finally paid off - Bun's HTTP server benchmarks at 200k+ req/s vs Node's 80k req/s (wrk benchmark, simple JSON response, M2 MacBook Pro)
- Deno Deploy runs Deno 2 at the edge — global distribution with no cold starts, built-in KV and queues
- All three now support Web APIs (fetch, WebSocket, ReadableStream) — the portability story is real
- Node.js weekly downloads: ~150M (npm), Bun: ~4M, Deno: ~1.5M (Dec 2025)
Runtime Landscape in 2026
Three years ago, Deno and Bun were experiments. Today, they're production options. The question isn't "should I try them?" — it's "which is right for my use case?"
The key shifts since 2024:
- Deno 2 dropped its anti-npm stance and now installs npm packages natively
- Bun 1.0+ shipped Windows support and stabilized its Node.js compatibility layer
- Node.js 22 LTS added native TypeScript type-stripping, and the 23/24 versions continued improving performance with V8 updates
This is no longer a "cutting edge vs production" story. All three are viable production runtimes.
Node.js 22: The Stable Foundation
Node.js 22 became LTS in October 2024 and is the current production standard. It ships V8 12.4+, full Web Streams API, native fetch(), and the experimental TypeScript stripping feature.
TypeScript Without a Build Step (Node 22.6+)
# Experimental TypeScript support — strips types without transpiling
node --experimental-strip-types index.ts
# With type checking disabled (just strips syntax)
# Note: tsconfig.json paths, decorators, and experimental features still need tsc
// index.ts — runs directly with node --experimental-strip-types
import { createServer, IncomingMessage, ServerResponse } from "http";
interface User {
id: number;
name: string;
email: string;
}
const users: User[] = [
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" },
];
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
if (req.url === "/users" && req.method === "GET") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(users));
} else {
res.writeHead(404);
res.end();
}
});
server.listen(3000, () => console.log("Server on :3000"));
Built-in Test Runner (no Jest needed)
// test.ts — node --test test.ts
import { test, describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
describe("User API", () => {
before(async () => {
// Setup test database
});
after(async () => {
// Cleanup
});
it("should return users", async () => {
const response = await fetch("http://localhost:3000/users");
assert.equal(response.status, 200);
const users = await response.json();
assert.ok(Array.isArray(users));
assert.equal(users.length, 2);
});
it("should have correct user shape", async () => {
const response = await fetch("http://localhost:3000/users");
const [user] = await response.json();
assert.ok("id" in user);
assert.ok("name" in user);
assert.ok("email" in user);
});
});
Permissions Model (Node 22.4+)
# Node.js now has an experimental permissions system
node --experimental-permission \
--allow-fs-read=/tmp \
--allow-net=api.example.com:443 \
--allow-child-process \
index.js
# Access denied at runtime for anything not listed
SQLite Built-In (Node 22.5+)
// node:sqlite — no npm package needed
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync(":memory:");
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
`);
const insert = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
insert.run("Alice", "alice@example.com");
insert.run("Bob", "bob@example.com");
const all = db.prepare("SELECT * FROM users").all();
console.log(all); // [{ id: 1, name: 'Alice', email: 'alice@example.com' }, ...]
Bun: The All-In-One Speed Machine
Bun is built in Zig using JavaScriptCore (the WebKit engine, same as Safari). It's designed to be the fastest JavaScript runtime and to replace your entire JS toolchain: npm + node + jest + esbuild in one binary.
Performance at a Glance
# Package installation benchmark (react + dependencies)
time npm install # ~12 seconds
time pnpm install # ~4 seconds
time bun install # ~0.8 seconds ← 15x faster than npm
# Script startup (hello world)
time node -e "console.log('hi')" # ~40ms
time bun -e "console.log('hi')" # ~8ms ← 5x faster
HTTP Server Performance
// Bun.serve — native HTTP server
const server = Bun.serve({
port: 3000,
fetch(req: Request): Response {
const url = new URL(req.url);
if (url.pathname === "/health") {
return Response.json({ status: "ok" });
}
if (url.pathname === "/users") {
return Response.json([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
]);
}
return new Response("Not Found", { status: 404 });
},
});
console.log(`Server running at http://localhost:${server.port}`);
// Benchmarks: ~220k req/s on Apple M2 (wrk -t4 -c100 -d30s)
// vs Node.js http module: ~85k req/s same hardware
Bun as Package Manager
# Drop-in replacement for npm
bun install # Install from package.json
bun add express zod # Add packages
bun add -d vitest # Dev dependency
bun remove lodash # Remove package
# Binary lockfile — faster than JSON
# bun.lockb — binary format, 3-5x smaller than package-lock.json
# Run scripts
bun run dev
bun run build
bun x tsc --version # Like npx, but faster
# Workspaces
bun install --filter my-app # Install only for specific workspace
Bun Test Runner
// Using bun test — drop-in replacement for Jest
import { expect, test, describe, beforeAll, afterAll, mock } from "bun:test";
describe("User service", () => {
beforeAll(() => {
// Setup — runs before all tests in this describe block
});
test("creates a user", async () => {
const response = await fetch("http://localhost:3000/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Charlie", email: "charlie@example.com" }),
});
expect(response.status).toBe(201);
const user = await response.json();
expect(user).toMatchObject({ name: "Charlie" });
expect(user.id).toBeNumber();
});
test("mocks external calls", () => {
const fetchMock = mock(() => Response.json({ mocked: true }));
globalThis.fetch = fetchMock;
expect(fetchMock).toHaveBeenCalledTimes(0);
});
});
Bun Built-In APIs
// File operations — faster than Node.js fs
const file = Bun.file("./data/users.json");
const users = await file.json();
await Bun.write("./output/result.json", JSON.stringify(users, null, 2));
// Environment — reads .env automatically
const PORT = Bun.env.PORT ?? "3000";
const DB_URL = Bun.env.DATABASE_URL; // No dotenv package needed
// Subprocess
const proc = Bun.spawn(["git", "log", "--oneline", "-10"], {
cwd: "/path/to/repo",
stdout: "pipe",
});
const output = await new Response(proc.stdout).text();
console.log(output);
// SQLite built-in (Bun ships bun:sqlite)
import { Database } from "bun:sqlite";
const db = new Database(":memory:");
db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
db.run("INSERT INTO users (name) VALUES (?)", ["Alice"]);
const users = db.query("SELECT * FROM users").all();
Bundler
// bun build — built-in bundler (replaces webpack/esbuild for many use cases)
await Bun.build({
entrypoints: ["./src/index.ts"],
outdir: "./dist",
minify: true,
sourcemap: "external",
target: "browser", // or "bun", "node"
splitting: true, // Code splitting
plugins: [
{
name: "replace-env",
setup(build) {
build.onLoad({ filter: /\.env$/ }, () => ({ contents: "" }));
},
},
],
});
Deno 2: Security-First, Now npm-Compatible
Deno 2 is the major pivot — it added npm compatibility, a package.json importer, Node.js built-in shims, and LSP improvements. The "no npm" philosophy is gone; Deno now embraces the npm ecosystem while keeping its security model and Web-first APIs.
npm and jsr Support
# npm packages work natively
deno run npm:express
deno run npm:fastify
# JSR (JavaScript Registry) — Deno's new registry
deno add @std/path
deno add jsr:@hono/hono
# No node_modules — packages cached in global dir
# Import maps make versions explicit
Deno.json Configuration
{
"tasks": {
"dev": "deno run --watch --allow-net --allow-env --allow-read main.ts",
"test": "deno test --allow-net --allow-read",
"build": "deno compile --allow-net --allow-env --allow-read main.ts"
},
"imports": {
"@std/path": "jsr:@std/path@^1.0",
"hono": "npm:hono@^4",
"zod": "npm:zod@^3"
},
"compilerOptions": {
"strict": true,
"lib": ["DOM", "DOM.Iterable", "deno.ns"]
}
}
HTTP Server with Hono on Deno
// main.ts — TypeScript runs natively, no compilation step
import { Hono } from "hono";
import { z } from "zod";
import { zValidator } from "@hono/zod-validator";
const app = new Hono();
const UserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
app.get("/users", (c) => {
return c.json([
{ id: 1, name: "Alice", email: "alice@example.com" },
]);
});
app.post("/users", zValidator("json", UserSchema), (c) => {
const user = c.req.valid("json");
return c.json({ id: Date.now(), ...user }, 201);
});
Deno.serve({ port: 3000 }, app.fetch);
Security Sandbox — Deno's Killer Feature
# By default, Deno denies ALL access
deno run main.ts # No network, no file system, no env
deno run --allow-net main.ts # Only network
deno run --allow-net=api.stripe.com main.ts # Only stripe.com
deno run --allow-read=/tmp main.ts # Only read /tmp
deno run --allow-env=DATABASE_URL main.ts # Only that one env var
deno run -A main.ts # Allow all (use cautiously)
# This is extremely useful for running untrusted scripts
// Deno permissions API — check at runtime
const netPermission = await Deno.permissions.query({
name: "net",
host: "api.stripe.com",
});
console.log(netPermission.state); // "granted", "denied", or "prompt"
Deno KV (Built-in Key-Value Store)
// Built into Deno Deploy — no Redis needed for simple use cases
const kv = await Deno.openKv();
// Write
await kv.set(["users", "user_123"], {
name: "Alice",
email: "alice@example.com",
createdAt: new Date().toISOString(),
});
// Read
const result = await kv.get<User>(["users", "user_123"]);
console.log(result.value?.name); // "Alice"
// Atomic operations
const txn = kv.atomic()
.check({ key: ["counter"], versionstamp: null }) // Only if doesn't exist
.set(["counter"], 0)
.commit();
// List with prefix
for await (const entry of kv.list<User>({ prefix: ["users"] })) {
console.log(entry.key, entry.value);
}
Deno Compile — Single Binary
# Compile to self-contained binary
deno compile --allow-net --allow-env --allow-read \
--output my-app \
main.ts
# Cross-compile for different targets
deno compile --target x86_64-unknown-linux-gnu main.ts
deno compile --target x86_64-pc-windows-msvc main.ts
deno compile --target aarch64-apple-darwin main.ts
Feature Comparison
| Feature | Node 22 LTS | Bun 1.x | Deno 2 |
|---|---|---|---|
| TypeScript (native) | Experimental (strip) | ✅ | ✅ |
| npm compatibility | ✅ 100% | ✅ ~95% | ✅ ~90% |
| Package manager | npm (separate) | ✅ Built-in | Built-in (deno add) |
| Test runner | ✅ node:test | ✅ bun:test | ✅ deno test |
| Bundler | ❌ (esbuild/webpack) | ✅ bun build | ✅ deno bundle |
| HTTP performance | ~85k req/s | ~220k req/s | ~150k req/s |
| Startup time | ~40ms | ~8ms | ~20ms |
| Install speed | Baseline | 10–25x faster | Moderate (global cache) |
| Security sandbox | Experimental (--perm) | ❌ | ✅ (deny-by-default) |
| Web APIs | ✅ (fetch, streams) | ✅ | ✅ (most complete) |
| Built-in SQLite | ✅ node:sqlite | ✅ bun:sqlite | Via npm:better-sqlite3 |
| Single binary compile | ❌ | ✅ bun compile | ✅ deno compile |
| Windows support | ✅ | ✅ (since 1.0) | ✅ |
| Ecosystem maturity | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Edge deployment | Lambda/Railway | Bun Cloud (beta) | Deno Deploy |
When to Use Each
Choose Node.js 22 LTS if:
- You need guaranteed compatibility with the full npm ecosystem (native addons, complex deps)
- Your team has years of Node.js expertise and switching creates more risk than value
- You're running in enterprise environments where "battle-tested" matters more than performance
- You need the widest range of hosting options (every cloud/PaaS supports Node.js)
Choose Bun if:
- Developer experience velocity matters — faster installs, faster tests, faster startup
- You're building a new project and want one tool for everything (no separate bundler, test runner, package manager)
- Your HTTP server is performance-sensitive and you want the extra headroom
- You're running on Linux/macOS (Windows support is good but slightly less mature)
Choose Deno if:
- Security is a first-class requirement — the deny-by-default sandbox is genuinely valuable
- You want to deploy at the edge (Deno Deploy is the most mature edge runtime platform)
- You're writing scripts or CLI tools where the permission system prevents accidental damage
- You appreciate the Web-standard APIs and want code that's maximally portable
The Compatibility Reality Check
npm ecosystem compatibility in practice (March 2026):
| Package category | Node 22 | Bun 1.x | Deno 2 |
|---|---|---|---|
| Pure JavaScript | ✅ | ✅ | ✅ |
| TypeScript | Via strip-types | ✅ Native | ✅ Native |
| Native addons (N-API) | ✅ | ✅ Most | ⚠️ Limited |
| Worker threads | ✅ | ✅ | ✅ |
| Child processes | ✅ | ✅ | ✅ (with --allow) |
| Popular frameworks | ✅ All | ✅ Express/Fastify/Hono | ✅ Express/Hono/Fastify |
| Prisma | ✅ | ✅ | ✅ |
| Drizzle | ✅ | ✅ | ✅ |
__dirname/__filename | ✅ | ✅ | ✅ (shimmed) |
Methodology
Benchmarks run on Apple M2 MacBook Pro (16GB RAM) running macOS Sequoia 15.3. HTTP benchmarks using wrk 4.2 (wrk -t4 -c100 -d30s) against a minimal JSON-returning endpoint. Install benchmarks with clean npm cache, measuring wall time. Runtime versions: Node.js 22.14 LTS, Bun 1.1.42, Deno 2.1.4. npm download data from npmjs.com public registry stats, December 2025. Star counts from GitHub as of February 2026.
See also: tsx vs ts-node vs bun: Running TypeScript Directly for TypeScript execution comparisons, or explore Express vs Hono vs Fastify for framework benchmarks on these runtimes.