Skip to content

Deno 2 vs Bun in 2026: The Runtime I Actually Reach For

Deno 2 vs Bun in 2026: The Runtime I Actually Reach For

Short version for the impatient: I ship most side projects on Bun. Client work that needs to still be alive in five years goes on Deno 2. Node 22 is still on the box for the codebases I inherited. If you only care about the punchline, that’s it.

The longer story is that I spent about six months last year picking one of these three for every new repo based on gut feel, and gut feel started disagreeing with itself. So I did what I usually do when I can’t stop context-switching: I sat down and wrote the trade-offs I actually cared about on a sticky note. That sticky note eventually turned into this post.

I still like all three runtimes. This isn’t a dunk piece. It’s just what I’ve settled on after enough production incidents to have opinions.

The setup: how I actually tested this

I’m not going to run a synthetic benchmark and pretend that decides anything. Every “X is 4× faster than Node” chart on Twitter is measuring http.createServer echoing a fixed string, which is not what any of us are shipping.

What I did instead: I took three of my own apps that I already had running on Node 22 and ported each to Bun 1.2 and Deno 2.2. The apps were a small Next.js dashboard with about 30 API routes on top of Postgres, a CLI tool that reads a directory of markdown and hits an LLM to write back annotations, and a background worker that reads a Kafka topic and writes to S3.

For each port I tracked cold start, memory, throughput, and the one I actually care about: how many hours I lost fighting the runtime instead of building the feature.

Bun 1.2 is still the “just works” runtime for greenfield

If I’m starting a project from bun init this weekend, it’s Bun. I keep trying to talk myself out of this because I like the idea of Deno’s security model, but the friction difference is real.

The install story is the whole game. bun install on my Next.js dashboard finished in 1.4 seconds cold. npm install on the same lockfile took 42. That’s not a benchmark, that’s just my terminal. The Bun install docs explain how the binary lockfile and the cached global store combine to get there. The practical effect is that I’ve stopped babysitting installs. I hit save and it’s done.

The other thing that keeps me on Bun is the test runner. It’s Jest-compatible enough that I can rip jest out of a repo, change three lines in package.json, and be done:

// Before: Jest with all its friends
{
  "scripts": { "test": "jest" },
  "devDependencies": {
    "jest": "^29",
    "ts-jest": "^29",
    "@types/jest": "^29"
  }
}

// After: bun test, nothing extra
{
  "scripts": { "test": "bun test" }
}

I wrote about that pattern in more detail in my year with Bun in production, and the same trick still lands.

Where Bun still costs me hours:

// This works on Bun. It does not work on Node.
import config from "./config.toml";

TOML imports, YAML imports, and the built-in SQLite are wonderful in demos and cursed in monorepos where half your tooling assumes Node semantics. My editor’s Node-based linter doesn’t know what to do with Bun.file(). Nothing breaks catastrophically, but I’ve spent whole afternoons chasing a red squiggle that would have been fine if I’d stayed on Node.

Rule I’ve settled on: Bun for anything I’m the only person touching. Bun for scripts. Not Bun for a codebase that a client’s IT will eventually inherit.

Deno 2 is the runtime I recommend when I won’t be on-call

Deno 2 shipped in late 2024 and it changed my calculus. Deno 1 was interesting but felt like it was fighting the ecosystem. Deno 2, per the official launch post, can install from npm the way you’d expect (deno add npm:express), reads package.json, and runs Node code without ceremony. It stopped feeling like a bet.

The reason I now reach for Deno 2 on client work is the permission model. When I hand off a codebase and the ops team asks “what does this thing access?”, I want to answer that from the CLI flags, not from grep. Compare:

# Bun or Node: trust whatever the code says
bun run worker.ts

# Deno 2: the runtime enforces the answer
deno run --allow-net=api.stripe.com --allow-env=STRIPE_KEY worker.ts

That second command is a security review in one line. I’ve had clients accept a Deno service in a week when the equivalent Node service was still going through infosec. That alone pays for the learning curve.

The other Deno 2 upgrade I care about is workspaces. I’ve been slowly rebuilding my monorepos on deno.json workspaces and the config is finally short enough that I stop typo’ing it. If you’re in monorepo land, the Deno workspace docs are the closest thing to a pnpm experience I’ve had without needing pnpm.

Where Deno 2 still costs me hours: Prisma. It works, technically, but I’ve hit generator edge cases twice this year that ate an afternoon each. If your ORM is Drizzle or plain sqlx-shaped code, Deno is fine. If your ORM assumes a Node build step, budget for surprises.

Node 22 LTS still runs most of what I actually maintain

I want to be honest: about 70% of the code I’ve shipped in the last year still runs on Node 22 LTS. Not because I love it, but because the codebases I’m hired to fix are already there and the ROI on a runtime port is almost never positive.

Node 22 is genuinely fine now. The built-in --watch mode killed my last reason to install nodemon. The node --test runner is Jest-shaped enough that I can delete a couple hundred lines of test config. And the Node permission model, still marked experimental as I write this, is quietly closing the gap on Deno’s headline feature.

Here’s the switch that finally landed for me on inherited codebases:

// Before: Node 20, jest, ts-node, nodemon
{
  "scripts": {
    "dev": "nodemon --exec ts-node src/index.ts",
    "test": "jest"
  },
  "devDependencies": {
    "nodemon": "^3",
    "ts-node": "^10",
    "jest": "^29",
    "@types/jest": "^29"
  }
}

// After: Node 22, nothing extra
{
  "scripts": {
    "dev": "node --watch --experimental-strip-types src/index.ts",
    "test": "node --test"
  }
}

I ripped four dev dependencies out of a client’s repo doing exactly this, and the build got 12 seconds faster. That’s not glamorous. It’s real work that ships.

The actual decision tree I use

If you’re standing in front of a new repo this week, this is what I do.

Is this a script or a solo weekend project? Bun. Faster install, faster startup, one binary to install for the next person who checks it out. Is this a service that a client will run in prod for three or more years? Deno 2. The permission model buys you a real audit story, and the standard library means fewer transitive deps. Is this an existing Node codebase with any team size above one? Stay on Node 22. Turn on --watch and node --test, delete the tooling you can, ship the feature.

And if you’re writing an internal LLM harness, honestly, Bun again. The startup time matters when you’re iterating on prompts and running the whole thing 200 times a day.

The pattern here is boring but it’s real. Runtime choice is rarely the thing that decides whether a project ships. Being ruthless about which runtime you’re paying attention to on any given day usually is.

What I’d try this week

If you’ve been meaning to try one of these, here’s the smallest useful experiment.

Take the smallest CLI script you have in a Node repo. Run bun install and bun run on it. See if anything breaks. If nothing does, you can start using Bun for scripts tomorrow.

If you’re about to spin up a service for a client, try deno init and read your way through the permission flags for one endpoint. It takes 20 minutes and it changes how you think about deps forever.

If you’re on Node 20 anywhere, upgrade to 22 today and delete nodemon. That’s the whole action.

I keep a rolling list of runtime trade-offs alongside the stacks I’m shipping on my work page if you want to see the versions I’m currently running in each project. The tl;dr hasn’t changed in six months, which is probably the strongest signal that these tools have finally stabilized.