I have shipped production code on all three runtimes in the last 18 months. A Node 20 LTS API for a logistics SaaS still doing 600 req/s; a Deno 2 edge handler for a marketing site that moved from Vercel to Deno Deploy when we hit cold-start ceilings; and a Bun monorepo for a startup where the founder convinced me Bun would shave deploy time, which it absolutely did. Each runtime is the right call somewhere; none of them is the right call everywhere.
The Node.js vs Deno vs Bun debate online is mostly about benchmarks. The real question for 2026 is which runtime fits which job, and what you give up when you pick the trendy one. The numbers and decision matrix below are the ones I now use with paying clients before they commit to a runtime they will live with for years.
The benchmark, with the test setup so you can replicate it
Tools: autocannon 7.x for load, single DigitalOcean droplet (2 vCPU, 4 GB), identical 64-byte JSON endpoint, no DB. Three back-to-back runs, median values. Versions: Node 22.4.0 LTS, Deno 2.0, Bun 1.2.
| Metric | Node 22 (Express 5) | Node 22 (Fastify 5) | Deno 2 (Hono) | Bun 1.2 (built-in) |
|---|---|---|---|---|
| Throughput (req/s) | 21,400 | 114,800 | 89,000 | 148,000 |
| p50 latency | 4.2 ms | 0.8 ms | 1.0 ms | 0.6 ms |
| RSS at idle | 54 MB | 61 MB | 72 MB | 43 MB |
| Cold start (first request) | 110 ms | 140 ms | 78 ms | 32 ms |
| npm install (small project) | 14 s | 14 s | 4 s (deno install) | 1.6 s (bun install) |
| Test runner cold start | 3.1 s (vitest) | 3.1 s (vitest) | 0.8 s (deno test) | 0.4 s (bun test) |
Two honest caveats. First, framework choice within Node moves the needle as much as the runtime choice — Fastify on Node ≈ Hono on Deno ≈ Bun built-in for raw HTTP. Second, real apps add a database, validation, and serialisation; the runtime gap shrinks to roughly 1.5× in my own production measurements, not the headline 7×.
What is wrong with picking based on benchmarks alone
Three production realities I have watched dilute the headline numbers:
- npm package compatibility is not 100% on Deno or Bun. Both claim Node compatibility. Both have edge cases — native modules, certain workspace patterns, libraries that probe
processin unusual ways. - Hosting and ecosystem maturity matter more than runtime speed. Node has 12 years of “deploy to anywhere” tooling. Deno is great on Deno Deploy and acceptable elsewhere. Bun runs anywhere but the production stories are still maturing.
- Hiring. Every backend engineer knows Node. Maybe one in three has touched Deno seriously. One in ten has shipped Bun. For a team of three, the trendy runtime is a luxury you can afford. For a team of thirty, it is a hiring tax.
What each runtime is, in two sentences
Node.js is V8 + libuv + the Node API surface, born 2009, the default JavaScript runtime everywhere outside the browser. In 2026 it ships native test runner, native fetch, native –watch, native –env-file — most of the reasons people reached for alternatives have been absorbed.
Deno is V8 + Rust runtime by the original Node author Ryan Dahl, born 2018, designed to fix Node’s mistakes (no node_modules, secure-by-default permissions, native TypeScript). Deno 2 (late 2024) added full npm and node: compatibility — the gap with Node closed dramatically.
Bun is JavaScriptCore + Zig runtime + bundler + package manager + test runner, born 2022, designed for speed. The package manager alone is worth the install: bun install is 10× faster than npm on most projects.
The same handler, three runtimes
// Node.js (Fastify)
import Fastify from 'fastify';
const app = Fastify();
app.get('/api/things', async () => ({ id: 1, name: 'thing' }));
app.listen({ port: 3000 });// Deno (Hono — works on Node and Bun too, runtime-agnostic)
import { Hono } from 'jsr:@hono/hono';
const app = new Hono();
app.get('/api/things', (c) => c.json({ id: 1, name: 'thing' }));
Deno.serve({ port: 3000 }, app.fetch);// Bun (built-in)
Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === '/api/things') {
return Response.json({ id: 1, name: 'thing' });
}
return new Response('Not found', { status: 404 });
},
});Three observations: Deno and Bun both expose the Web Fetch API directly (Request/Response), Node now does too (fetch, Response.json()), and the framework choice matters more than the runtime for handler shape.
TypeScript story: who is native, who needs help
| Runtime | TypeScript | Type checking |
|---|---|---|
| Node 22 | Native (since 22.6, experimental flag-gated) | Strip-only by default; use tsc --noEmit for checks |
| Deno 2 | Native, default | Full type-check on every run unless disabled |
| Bun 1.2 | Native, default | Strip-only; use tsc --noEmit for checks |
Honest take: Node’s TypeScript support catching up to Bun and Deno was the headline feature of Node 22. For day-to-day development, all three feel native enough. The CI step still runs tsc --noEmit on all three because runtime “TypeScript” support means strip-the-types, not check-the-types.
Package manager and dependencies
| Runtime | Default manager | Speed (10k deps) | Lock file |
|---|---|---|---|
| Node | npm / pnpm / yarn | 14–60 s (npm), 6 s (pnpm) | Yes, all three |
| Deno | JSR + npm via npm: specifier |
4 s (no install for jsr.io modules) | deno.lock |
| Bun | bun install (npm-compatible) | 1.6 s | bun.lockb (binary) |
Bun’s package manager is the underrated win. It works on any project, including pure Node — you can use bun install in a project that runs on Node, get the install speed, and keep your runtime. Bun install docs for the migration.
Production hosting reality
| Where | Node | Deno | Bun |
|---|---|---|---|
| VPS (DigitalOcean / Hetzner / Linode) | First-class | Works, fewer guides | Works, growing guides |
| Vercel | First-class | Vercel Edge supports it | Beta (Bun runtime preview) |
| AWS Lambda | First-class | Works, custom runtime | Works, custom runtime |
| Cloudflare Workers | Limited (no Node API) | Native (workerd / Deno-compatible) | Limited |
| Deno Deploy | — | Native, free tier | — |
| Fly.io / Railway / Render | First-class | Works via Dockerfile | Works via Dockerfile |
The “deploy anywhere” advantage is still Node’s. Most managed platforms support Node natively; Deno and Bun usually need a custom Dockerfile.
Migration path: when you might switch
Three migration shapes I have shipped or watched:
- Node → Bun for monorepo install speed. Lowest risk, highest ROI. Keep your code on Node, swap
npm installforbun install. CI builds drop from 4 minutes to 90 seconds. Some packages misbehave; mostly fine. - Node → Deno for edge deployment. Project moves to Deno Deploy or Cloudflare Workers because cold starts on Lambda/Vercel got expensive. Code rewrite is small if you used Hono or Fetch-API frameworks; large if you used Express.
- Node → Bun for full runtime. Highest risk. Native module compatibility is the wild card; some libraries (sharp, node-canvas, certain database drivers) work most of the time and fail in interesting ways at scale. Pilot for two months on a non-critical service before committing.
Decision matrix: which one to pick
| Pick Node when | Pick Deno when | Pick Bun when |
|---|---|---|
| You ship to a long-running VM or container. | You deploy to the edge (Cloudflare, Deno Deploy). | Install speed and bundling matter. |
| You need the deepest npm compatibility. | You want secure-by-default permissions. | You start greenfield and accept ecosystem risk. |
| You hire backend engineers from the open market. | You write internal tools that benefit from JSR’s package model. | You can pilot on non-critical services first. |
| You run native modules (sharp, sqlite, native crypto). | You want native TypeScript with full type checking. | You like having a bundler + test runner + package manager in one tool. |
| You build serverless on AWS Lambda. | You want first-class Web standards on the server. | You ship server-rendered apps where startup time dominates UX. |
Production checklist when you commit to a non-Node runtime
- Pilot on a non-critical service for 60 days minimum. Native module compatibility surfaces under load, not in dev.
- Lock your runtime version explicitly —
"engines"in package.json (Node) or pinned binary in Dockerfile (Deno, Bun). - CI tests against the production runtime. Deno tests passing locally on
deno testdoesn’t validate yournode:*imports if production runs them via npm specifiers. - Build a Dockerfile that you control. Managed platforms abstract the runtime; you want to own the binary version.
- Have a Node fallback plan for libraries that turn out to misbehave. Most apps can run on Node with one config flip; design for it.
- Monitor cold starts after deploy. Bun’s cold-start advantage shows up at scale; if your infra introduces an extra layer, you may not see it.
When the trendy choice is wrong
Three honest cases where the trendy runtime is not the right call:
- You inherited a 5-year-old Node monolith that ships and earns. The migration cost will not pay back unless you have a measured bottleneck the runtime change actually fixes.
- Your team is more than 10 engineers. Node’s hiring pool is 10× larger than Bun’s. The runtime tax of “we’re a Bun shop” shows up in every onboarding.
- You depend on Sentry, OpenTelemetry, New Relic, Datadog APM with the deepest features. Observability tooling is fully baked on Node and uneven on Deno/Bun. Outages get longer when your APM lies.
Troubleshooting FAQ
Is Bun production-ready in 2026?
For greenfield projects with limited native module dependencies, yes. The runtime is stable; the ecosystem is still maturing. Bun ships with Discord, Anthropic, and several fintech startups in production.
Can Bun replace Node entirely?
Functionally close but not identical. Some npm packages with native bindings (sharp, certain database drivers) work most of the time and fail in interesting ways. Pilot before committing.
Is Deno still relevant after Node added what it pioneered?
Yes, in two niches: edge deployment via Deno Deploy or Cloudflare Workers, and the JSR package registry’s superior versioning model. For VPS-style backends, Node is still the safer pick. For AI-integration workloads, Node’s SDK ecosystem (OpenAI, Anthropic, Vercel AI SDK) is most mature.
Should I use Bun for the package manager and Node for the runtime?
Yes — this is genuinely useful. bun install on a Node project is fast and compatible; you keep Node’s runtime ecosystem and get Bun’s installer speed. I do this on most new Node projects.
Do Express and Fastify work on Bun?
Fastify works well on Bun; Express works with caveats. Bun’s built-in server is faster than both, but if you need Express middleware ecosystem, you sacrifice some of Bun’s speed advantage.
What about Cloudflare Workers — that uses workerd, right?
Yes, workerd is the V8-based runtime Cloudflare ships. It’s spiritually closer to Deno than Node — Web Fetch API, no node:* by default (though nodejs_compat flag enables many). Treat it as a fourth option, not a flavour of Node.
Does TypeScript compile faster on Bun?
Bun’s TypeScript handling is strip-only (same as Node 22), so the “faster TypeScript” headline is about test runner speed, not type-checking speed. tsc --noEmit takes the same time on all three runtimes.
Will Node be deprecated soon?
No. The OpenJS Foundation backs Node; corporate sponsors include Google, Microsoft, IBM, Red Hat. Node ships LTS lines on a 30-month support window. Deno and Bun are competition, not replacements.
Verdict
For most paying client work in 2026, my default is still Node 22 LTS with Fastify or Express, packaged with bun install for speed. Boring, well-supported, hires easily, deploys anywhere.
For edge-deployed apps where cold starts matter, Deno 2 on Deno Deploy or Cloudflare Workers. The runtime is mature; the platform is excellent.
For greenfield startups where deploy speed and tooling consolidation matter more than ecosystem depth, Bun 1.2. The package manager alone is worth the install; the rest is a bet that pays off in the right shape of project.
The wrong question is “which is fastest?” The right question is “what does my deployment shape look like, and what is my team’s risk tolerance for ecosystem maturity?”