Choosing a server-side JavaScript runtime used to mean picking Node.js and moving on. In 2026, all three runtimes run TypeScript, ship a test runner, support Web-standard APIs, and install npm packages. The differences now sit in security defaults, startup and I/O performance, tooling surface area, and long-term support guarantees. This guide breaks down each runtime with runnable code, then gives you a decision framework.
Quick Takeaways
- Node.js is the safest default for production. Node.js 26 was released May 5, 2026, and 24 and 22 are LTS lines. Node 26 is scheduled to enter LTS this month.
- Deno has the strongest security model and the cleanest built-in toolchain. Deno 2.9.7 (September 16, 2026) is the latest supported release.
- Bun wins on all-in-one tooling and raw speed, with the largest maturity trade-off. Bun 1.4.2 shipped on September 5, 2026.
- For new projects, write against Web-standard APIs (
fetch,Request,Response,URL). That keeps you portable across all three.
| Criterion | Node.js 26 / 24 LTS | Deno 2.9 | Bun 1.4 |
|---|---|---|---|
| Engine | V8 | V8 | JavaScriptCore |
| Implementation language | C++ | Rust | Rust (rewritten from Zig) |
| Native TypeScript | Type stripping (erasable syntax) | Full, built-in | Full, built-in |
| Security model | Opt-in --permission flag |
Deny-by-default permissions | No sandbox by default |
| npm compatibility | Native | Strong, via npm: and package.json |
Strong, drop-in |
| Built-in tooling | Test runner, watch mode | Lint, fmt, test, bench, compile, desktop | Bundler, test runner, SQL, Redis, cron |
| Ecosystem maturity | Highest | High | Moderate |
| Best for | Enterprise, long-lived services | Secure scripts, edge, greenfield TS | Fast tooling, scripts, startups |
The 2026 Landscape: What Changed
Three shifts reshaped the comparison this year.
Node.js changed its release model. Starting with Node.js 27, the project moves from two major releases per year to one, and every release becomes LTS. Odd/even version rules are retiring. Node.js 26 is the last line under the old model.
Deno stopped chasing a “3.0”. There is no Deno 3 on the roadmap. The 2.x line carried the npm compatibility work instead. Deno 2.9 added deno desktop for packaging native desktop apps and made deno install read npm, pnpm, yarn, and Bun lockfiles directly.
Bun was acquired and rewritten. Anthropic acquired Bun in December 2025 and later ported the codebase from Zig to Rust, shipping it as Bun v1.4.
Disclosure: This article was drafted with Claude, an Anthropic model, and Anthropic owns Bun. Treat the Bun commentary with the same scrutiny as the others, and verify claims against the official changelogs.
Syntax Breakdown: The Same HTTP Server in Three Runtimes
Start with the simplest comparison. Each snippet below serves JSON on port 3000.
Node.js
// server.ts — run with: node server.ts
import { createServer } from "node:http";
const server = createServer((req, res) => {
// Set headers explicitly; Node does not infer JSON content types
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ runtime: "node", version: process.version }));
});
server.listen(3000, () => console.log("Listening on :3000"));
Output: Visiting http://localhost:3000 returns {"runtime":"node","version":"v26.x.x"}. Node strips the type annotations at load time, so no tsc or tsx step is needed for this file. Type stripping handles only erasable syntax. Features that emit runtime code, such as enum and namespace, need an extra flag or a refactor.
Deno
// server.ts — run with: deno run --allow-net server.ts
Deno.serve({ port: 3000 }, () => {
// Response.json() is a Web-standard helper
return Response.json({ runtime: "deno", version: Deno.version.deno });
});
Output: The same JSON shape. Without --allow-net, Deno prompts for permission or exits with a PermissionDenied error. The handler is a plain function from Request to Response, with no callback juggling.
Bun
// server.ts — run with: bun server.ts
Bun.serve({
port: 3000,
fetch() {
return Response.json({ runtime: "bun", version: Bun.version });
},
});
Output: The same JSON, with no flags and no install step. Bun and Deno both converged on the Fetch API handler pattern. Node’s classic http module is the outlier, which matters when you plan for portability.
Runtime Deep Dive
Node.js: The Production Baseline
Node.js has the largest package ecosystem, the broadest hosting support, and the most battle-tested libuv event loop. The 2026 release cycle is worth planning around.
| Line | Status (October 2026) | End of life |
|---|---|---|
| Node 26 | Current, entering LTS this month | April 2029 |
| Node 24 “Krypton” | LTS | April 2028 |
| Node 22 “Jod” | Maintenance LTS | April 2027 |
Node 22’s security support ends April 30, 2027, and Node 26 is slated to run through April 2029. If you still run Node 20 or older, you are on an unpatched line.
Node has also absorbed features that once justified switching runtimes: a built-in test runner, --watch mode, node:sqlite, type stripping, and a permission model behind the --permission flag. Node 26 also ships the Temporal API and V8 14.6.
Choose Node when: you need maximum library compatibility, vendor-certified hosting, long LTS windows, or native addons.
Deno: Secure by Default
Deno’s defining feature is its permission system. A script cannot read files, open sockets, or read environment variables unless you grant access.
# Allow network on one port and read access to one directory only
deno run --allow-net=:3000 --allow-read=./data server.ts
Output: If the script tries to write to disk or call an unlisted host, Deno throws PermissionDenied instead of silently succeeding. That limits the damage from a compromised dependency.
Deno’s 2026 releases focused on closing the gap with Node. Deno 2.9 raised its Node.js compatibility target to Node.js 26. The min-release-age option refuses npm versions younger than a set age, which is a cheap defense against freshly poisoned packages.
A project can mix package.json dependencies with Deno-native imports:
// main.ts
import express from "npm:express@5"; // npm package via specifier
import { assertEquals } from "jsr:@std/assert"; // JSR standard library
const app = express();
app.get("/", (_req, res) => res.json({ ok: true }));
app.listen(3000);
assertEquals(1 + 1, 2); // quick sanity check at startup
Output: Express 5 serves on port 3000 under Deno. The npm: specifier resolves and caches the package without a separate install step.
Choose Deno when: you run untrusted or third-party code, want one binary for lint, format, test, and deno compile, or deploy to edge platforms.
Bun: Speed and Batteries Included
Bun bundles a runtime, package manager, bundler, and test runner. It uses JavaScriptCore instead of V8, which gives it fast startup.
// db.ts — run with: bun db.ts
import { Database } from "bun:sqlite";
const db = new Database(":memory:");
db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
const insert = db.prepare("INSERT INTO users (name) VALUES (?)");
insert.run("Ada");
// .get() returns the first matching row, or null if none match
const row = db.query("SELECT * FROM users WHERE name = ?").get("Ada");
console.log(row); // { id: 1, name: "Ada" }
Output: Prints { id: 1, name: "Ada" }. SQLite needs no install because it ships inside the runtime. Bun also bundles a SQL client and a Redis client, and its 2026 releases added in-process cron and parallel test execution.
The Bun story has caveats. The Rust port merged in May 2026 as a single commit of over a million lines. That raises fair questions about long-term maintainability. Some developers have complained about memory usage and open issues, and about Claude Code’s dependency on Bun. Test your workload under memory pressure before committing.
Choose Bun when: you want the fastest install and script startup, a single-tool workflow, or you are building tooling and internal services where raw speed matters more than ecosystem depth.
Performance: What the Numbers Mean
Benchmarks vary by workload, so use these patterns as a guide rather than a guarantee.
| Workload | Typical leader | Why |
|---|---|---|
| Cold start (CLI/scripts) | Bun | JavaScriptCore starts quickly |
| Package install | Bun, then Deno | Native installers and global caches |
| Long-running HTTP throughput | Close; varies by framework | All three are I/O-bound in real apps |
| CPU-heavy JS | Node / Deno (V8 JIT) | Mature optimizing compiler |
| Memory stability under load | Node | Longest production track record |
Real applications spend most of their time waiting on databases and networks. A runtime that is 20% faster on a synthetic “hello world” often changes nothing for a service dominated by O(n) database round trips. Profile your own workload before migrating for speed.
TypeScript Support Compared
| Feature | Node.js | Deno | Bun |
|---|---|---|---|
Run .ts directly |
Yes (type stripping) | Yes | Yes |
| Type checking | No; run tsc --noEmit |
Optional via deno check |
No; run tsc --noEmit |
enum / namespace |
Needs a transform flag | Supported | Supported |
tsconfig.json paths |
Limited | Via import maps | Supported |
No runtime type checks your code during normal execution except Deno’s deno check. Keep tsc --noEmit in CI regardless of runtime.
Bad Code vs. Good Code: Writing Portable Runtime Code
Coupling your business logic to a runtime-specific global is the most common migration blocker.
Anti-pattern
// ❌ Bad: business logic welded to Bun globals
export async function loadConfig() {
const file = Bun.file("./config.json"); // Bun-only
const cfg = await file.json();
return { ...cfg, port: Bun.env.PORT }; // Bun-only
}
This code fails on Node and Deno with a ReferenceError.
Refactored
// ✅ Good: Web-standard handler plus a thin runtime adapter
// app.ts — pure, portable logic
export async function handle(req: Request): Promise<Response> {
const url = new URL(req.url);
if (url.pathname === "/health") {
return Response.json({ status: "ok" });
}
return new Response("Not Found", { status: 404 });
}
// entry.bun.ts → Bun.serve({ fetch: handle });
// entry.deno.ts → Deno.serve(handle);
// entry.node.ts (using @hono/node-server as the adapter)
import { serve } from "@hono/node-server";
import { handle } from "./app.ts";
serve({ fetch: handle, port: 3000 });
Result: handle has no runtime imports, so it is unit-testable and portable. Only the three-line entry files change per runtime. Read environment variables through process.env, which all three support, instead of runtime-specific APIs.
Decision Framework
| Your situation | Pick | Reason |
|---|---|---|
| Enterprise service, compliance audits | Node.js LTS | Longest support windows, widest vendor support |
| Running untrusted or plugin code | Deno | Deny-by-default permissions |
| Greenfield TypeScript API | Deno or Bun | No build step, strong built-in tooling |
| CI scripts and monorepo tooling | Bun | Fast installs and startup |
Native addons (node-gyp) |
Node.js | Best addon compatibility |
| Desktop app from a web stack | Deno 2.9 | deno desktop produces a single binary |
| Maximum hiring pool | Node.js | Most developers know it |
Migration Checklist
- Audit dependencies for native addons and
node:built-ins. These cause most breakage. - Run your existing test suite under the new runtime before changing any code.
- Replace runtime-specific globals with Web-standard APIs.
- Pin the runtime version in CI and in your container image.
- Load-test memory and latency under production-like traffic.
Deno 2.9 makes step 1 easier, since deno install can seed a deno.lock from your existing lockfile.
Frequently Asked Questions
Is Bun faster than Node.js in 2026?
Bun is usually faster at startup, package installs, and test execution. In long-running servers the gap narrows, because database and network latency dominate. Benchmark your own workload.
Is Deno ready to replace Node.js in production?
For many greenfield services, yes. Deno 2.x supports npm packages and package.json, and tracks Node 26 compatibility. Legacy apps with native addons or unusual node: internals may still need Node.
Which JavaScript runtime is best for TypeScript?
All three run .ts files directly. Deno and Bun support the full language with no flags. Node handles erasable syntax by default and needs a flag for enum and namespace. None replaces tsc --noEmit for type checking.
Which runtime should beginners learn first?
Start with Node.js. Its documentation, tutorials, and job market are the largest, and the skills transfer directly to Deno and Bun.




