Skip to content

What Actually Changes When You Upgrade to Deno 2.9

Deno 2.9 disables Deno.serve automatic compression by default, cuts startup from 22ms to 15ms, and now reports Node v26.3.0. Here is the upgrade path.

· · 4 min read
Lines of colorful JavaScript code displayed on a dark screen

Quick Take

Most of the 2.9 release notes are additions you can ignore until you need them. One line is not: automatic response compression is off now, and nothing in your code will tell you.

One Line Matters More Than the Rest

Deno 2.9 landed on 25 June 2026 with a long list of additions: deno watch, deno link, snapshot testing, a desktop packaging command, Happy Eyeballs for outbound connections. Nice to have. None of it will wake you up.

This will:

Disable Deno.serve automatic compression by default

That is a breaking change written as a bullet point, and it does not throw. Your server keeps serving. Your responses just stop being compressed, and the first person to notice is a user on mobile data, not your test suite.

What That Costs You

Compression is not a rounding error on text payloads. A JSON API response of any size typically gives up somewhere between 60 and 80 percent of its bytes to gzip, and HTML does better than that. Turning it off silently means a response that was 40KB on the wire is suddenly 150KB, at exactly the same status code, with exactly the same body your assertions check.

So the fix is to be explicit. If something sits in front of your app, put it there:

# nginx, if you have one in the path already
gzip on;
gzip_types application/json text/html text/css application/javascript;

If nothing does, do it in the handler:

Deno.serve((req) => {
  const body = JSON.stringify({ hello: "world" });
  const accepts = req.headers.get("accept-encoding") ?? "";
  if (!accepts.includes("gzip")) {
    return new Response(body, { headers: { "content-type": "application/json" } });
  }
  const stream = new Response(body).body!.pipeThrough(new CompressionStream("gzip"));
  return new Response(stream, {
    headers: { "content-type": "application/json", "content-encoding": "gzip" },
  });
});

CompressionStream is a web standard and has been available in Deno for a long time, so this needs no dependency. Check the request header before compressing, because a client that did not ask for gzip will not decode it.

Why did they do this? I would guess double-compression. When the runtime compresses and a reverse proxy compresses again, you burn CPU twice and occasionally produce something a client mishandles. Making it opt-in pushes the decision to whoever actually knows the deployment topology. That is defensible. It is still a change you have to act on.

The Node Version Now Reads 26.3.0

process.version reports v26.3.0 in 2.9. This sounds cosmetic and is not.

Plenty of packages branch on the reported Node version. Some check for a minimum. Some enable a fast path above a threshold. Some, less helpfully, refuse to run above one. Grep your dependencies before assuming the upgrade is transparent:

grep -rn "process.version" node_modules --include="*.js" | head -20

That is a blunt instrument and it will produce noise, but it takes ten seconds and occasionally finds the package that is about to behave differently for reasons unrelated to anything you wrote.

Alongside it: Node-API version 10, node:test gaining mock.module and mock.timers, process.resourceUsage(), and better ESM hooks for import attributes. The direction is unmistakable. Deno is no longer positioning itself as the alternative to Node so much as a runtime that runs Node's code and reports Node's version.

Startup: 22ms to 15ms

The release claims startup fell from 22ms to 15ms, with WebCrypto ported from JavaScript to Rust, console and inspect likewise, plus lazy loading of node:buffer and node:timers globals.

Seven milliseconds. For a server, meaningless. For anything invoked repeatedly, it adds up in a way worth measuring:

hyperfine --warmup 3 'deno run --allow-read script.ts'

A pre-commit hook that runs four scripts saves about 28ms per commit. A CI matrix that spawns the binary two thousand times saves fourteen seconds. Neither is thrilling on its own. Both are free.

I would not upgrade for this. I would take it while upgrading for the compression fix, which you have to do anyway.

The Lockfile Change Is The Best Migration News

Buried under the CLI additions: deno.lock can now be seeded from an existing npm, yarn, pnpm or bun lockfile.

If you have ever tried moving a real project onto Deno, you know why that matters. Starting from a fresh resolve means every transitive dependency picks whatever satisfies the range today, which is not what your Node app has been running in production for eight months. Seeding from the existing lockfile starts you from versions that are known to work together, and reduces migration to a question of runtime APIs rather than a dependency archaeology project.

The same release also auto-resolves git merge conflicts in deno.lock. Small, unglamorous, and it removes one of the reliably annoying parts of rebasing on a repository where more than two people add dependencies.

What I Would Actually Do

Upgrade, but treat it as a change rather than a bump:

  1. Add explicit compression, or confirm your proxy already does it. Verify with curl -H 'Accept-Encoding: gzip' -I your-endpoint and look for content-encoding.
  2. Grep dependencies for process.version checks.
  3. Seed deno.lock from your existing lockfile if you are mid-migration.
  4. Ignore deno desktop unless you are shipping a desktop app, which you are probably not.

If you are still weighing runtimes rather than upgrading one, the Bun vs Node vs Deno comparison covers where each one currently makes sense, and 2.9 does not change those conclusions much.

The uncomfortable summary: the headline features of this release are the ones you will never use, and the one that will affect your users is a single line about compression that reads like housekeeping.

Frequently Asked Questions

Does upgrading to Deno 2.9 break my HTTP server?
It will not throw, and that is the problem. Deno.serve no longer applies gzip or brotli automatically, so responses keep working and simply arrive larger. If you were relying on the runtime to compress JSON or HTML, your payloads grow and your users notice before your tests do. Add compression explicitly, either in your handler or at whatever proxy sits in front.
What Node version does Deno 2.9 report?
process.version reports v26.3.0. That matters if any dependency gates behaviour on the reported Node version, which a surprising number of packages still do. Check anything that branches on process.version or semver-compares it before you assume the upgrade is transparent.
Is the startup improvement real?
The release notes claim 22ms down to 15ms, roughly a third. For a long-running server that is irrelevant. For a CLI tool, a pre-commit hook, or anything a CI job invokes hundreds of times, it compounds into real minutes. Measure your own case with hyperfine rather than trusting either the number or my summary of it.
Can I import my existing npm lockfile?
Yes, and this is the quietly useful one. Deno 2.9 can seed deno.lock from an existing npm, yarn, pnpm or bun lockfile, so a migration starts from resolved versions instead of a fresh resolve. It also auto-resolves git merge conflicts in deno.lock, which removes one of the more tedious rebase chores on a busy repository.