Skip to content
Nilay Kabariya

Entry No.066·Deploy·Updated ·3 min read

Deno Deploy shutdown: move to Workers or Node, tested

Deno Deploy closes in about six months. I moved a typical Deno Deploy app to Cloudflare Workers and to Node 26: every error, and what fixed each.

by Nilay#cloudflare#workers#wrangler#nodeDEPLOY

The error, verbatim

Jump to the fix ↓
Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. Received protocol 'jsr:'
✓ Reproduced on Windows 11 Pro; Ubuntu on GitHub Actions

Tested on

Node
26.11.1
Wrangler
4 (wrangler dev, local)
Deno
2.9.6 (baseline only)
Re-run
November 3: Deno 2.9.8, Node 26.13.0, wrangler 4.156.2 (Linux)
OS
Windows 11 Pro; Ubuntu on GitHub Actions

✓ Last re-run on GitHub Actions · see the run · all repros

Contents
  1. Step 1, both targets: replace jsr: imports01
  2. Moving to Node 2602
  3. Moving to Cloudflare Workers03
  4. What I ran, and when04

On October 9, 2026, the Deno team announced it’s joining Cloudflare. Deno Deploy will keep running for six months and then shut down, which puts the end around April 2027. The Deno runtime gets one more year of monthly bug-fix and security releases. JSR stays up, moving to Cloudflare’s infrastructure. Neither announcement includes a migration guide; Deno offers migration help only to paying customers moving to Workers.

So I took the kind of app Deno Deploy is made for and moved it. It uses the four things almost every Deno Deploy app uses: Deno.serve, Deno.env, Deno KV and a jsr: import:

import { Hono } from "jsr:@hono/hono@4";

const kv = await Deno.openKv();
const app = new Hono();

app.get("/", (c) => c.text(`hello from ${Deno.env.get("APP_NAME") ?? "deno"}`));
app.get("/count", async (c) => {
  const cur = (await kv.get<number>(["visits"])).value ?? 0;
  await kv.set(["visits"], cur + 1);
  return c.json({ visits: cur + 1 });
});

Deno.serve({ port: 8787 }, app.fetch);

On Deno 2.9.6 it worked when run with --unstable-kv (without that flag, Deno itself fails with Deno.openKv is not a function). Run unchanged anywhere else, it fails before serving a single request:

Running it on First error
Node 26 ERR_UNSUPPORTED_ESM_URL_SCHEME … Received protocol 'jsr:'
Cloudflare Workers (wrangler dev) Could not resolve "jsr:@hono/hono@4" and Top-level await is currently not supported with the "iife" output format

Step 1, both targets: replace jsr: imports

Neither Node nor Wrangler understands jsr: URLs. JSR publishes every package to an npm-compatible registry, and its CLI adds them to package.json:

npx jsr add @hono/hono@4

Then import by name, without the jsr: prefix:

import { Hono } from "@hono/hono";

With the import fixed, the Deno global is next. On Node 26: ReferenceError: Deno is not defined. On Workers the build still stops at the top-level await Deno.openKv().

Moving to Node 26

Three replacements: Deno.serve → @hono/node-server, Deno.env.get → process.env, and Deno.openKv → the official @deno/kv npm package, which has the same API and stores data in a local SQLite file:

npm i @hono/node-server @deno/kv
import { Hono } from "@hono/hono";
import { serve } from "@hono/node-server";
import { openKv } from "@deno/kv";

const kv = await openKv("kv.sqlite");
const app = new Hono();

app.get("/", (c) => c.text(`hello from ${process.env.APP_NAME ?? "node"}`));
app.get("/count", async (c) => {
  const cur = (await kv.get<number>(["visits"])).value ?? 0;
  await kv.set(["visits"], cur + 1);
  return c.json({ visits: cur + 1 });
});

serve({ fetch: app.fetch, port: 8787 });

The data lives in kv.sqlite on that one machine. That’s fine for a single server, not for several. Your existing Deno KV data stays on Deno Deploy, so export it while Deploy is still running.

Moving to Cloudflare Workers

A Worker doesn’t start a server. It exports a fetch handler, gets settings and storage through env bindings, and can’t run await at the top level.

Export the app instead of serving it, read settings from c.env, and use Workers KV:

import { Hono } from "@hono/hono";

type Env = { APP_NAME?: string; KV: KVNamespace };
const app = new Hono<{ Bindings: Env }>();

app.get("/", (c) => c.text(`hello from ${c.env.APP_NAME ?? "workers"}`));
app.get("/count", async (c) => {
  const cur = Number((await c.env.KV.get("visits")) ?? 0);
  await c.env.KV.put("visits", String(cur + 1));
  return c.json({ visits: cur + 1 });
});

export default app;
{
  "name": "deno-app",
  "main": "main.ts",
  "compatibility_date": "2026-10-01",
  "vars": { "APP_NAME": "workers" },
  "kv_namespaces": [{ "binding": "KV", "id": "local-dev-only" }]
}

Before deploying, create a real namespace with npx wrangler kv namespace create KV and put its id in place of local-dev-only.

What I ran, and when

Windows 11 Pro, October 14, 2026. The original app ran on Deno 2.9.6 (from npm, as a baseline). The ports ran on Node 26.11.1 and in Wrangler 4’s local wrangler dev, with nothing deployed to a Cloudflare account. Every error above is the real output.

On November 3, 2026, the whole move ran again on Linux (GitHub Actions ubuntu-latest) with Deno 2.9.8, Node 26.13.0 and wrangler 4.156.2, using wrangler dev only, so nothing was deployed. Every step matched: Deno needed --unstable-kv; the unchanged file failed on Node 26 with ERR_UNSUPPORTED_ESM_URL_SCHEME and in wrangler dev with the jsr: and top-level await errors; npx jsr add @hono/hono without a version still picked 5.0.0-rc.1, and with @4 it added ^4.13.16; and both migrated versions answered / and counted 1, then 2. The script and the run are public.

— N.K., end of entry No.066

Useful? Pass it on:Post on XFollow @EmotionalMatter

Related entries

  1. No.025

    Fix: Error 1101 Worker threw exception (Cloudflare Workers)

    Cloudflare's 1101 page hides the real error. See it with one command, plus six causes reproduced on a deployed Worker, each with its exact log message.

    > Error 1101

    DEPLOY3 min
  2. No.033

    Fix: Script startup exceeded CPU time limit (Workers 10021)

    Cloudflare rejects your Worker deploy with Script startup exceeded CPU time limit. How to see which code is slow, and the fix that removes it, measured.

    > X [ERROR] Your Worker failed validation because it exceeded startup limits.

    DEPLOY2 min
  3. No.032

    Fix: The uploaded script has no registered event handlers

    Cloudflare rejects your Worker with no registered event handlers (code 10068), or accepts it and serves a 404. Seven handler shapes tested, Hono included.

    > X [ERROR] A request to the Cloudflare API (/accounts/.../workers/scripts/nk-repro-cpu) failed.

    DEPLOY3 min

Post card · Every Sunday

What broke this week.

One email on Sundays: the Node, TypeScript, Python and AI SDK releases that broke builds that week, each one reproduced, with a link to the fix that worked. Never shared, and you can leave any time.

— Nilay

or follow by RSSor on X

By subscribing you agree to the privacy note. One click to leave.

tip: paste the exact error text