DenoKV on the Fly.io + Queues

Deno Deploy Classic shuts down on July 20, 2026. I’ve been using Deno KV in a few small projects and the new platform has a couple of breaking surprises waiting for you in the migration guide:

  • your KV data is not migrated automatically
  • Deno Queues (enqueue / listenQueue) are simply not supported on the new platform
  • the new platform uses per-timeline database isolation, which changes how KV databases are scoped

So I built a small, ready-to-deploy setup that runs the official open-source denokv binary on Fly.io - the same SQLite-backed server Deno uses internally, exposed over the KV-Connect protocol. Your application code doesn’t change at all:

const kv = await Deno.openKv("https://your-app.fly.dev");

What’s inside

The container runs three processes under supervisord:

  1. Litestream - continuously replicates the SQLite WAL to an S3 bucket (I use Fly’s built-in Tigris). On cold boot it restores from the latest snapshot automatically.
  2. denokv - waits for the database to be ready, then serves KV-Connect on port 4513.
  3. deno - proxy to 4512 and serve /lib files.
  4. Supercronic - runs a daily cron job at 02:00 UTC that restores the latest backup into /tmp, runs PRAGMA integrity_check, and sends a pass/fail e-mail via msmtp.

The data sits on a 1 GB Fly persistent volume. The whole VM costs about $2–3/month on a shared-cpu-1x.

Why the daily backup check?

Litestream is continuous replication, not a traditional backup. It’s extremely reliable, but I wanted an independent signal: every night something actually restores the snapshot from S3 and verifies it. If anything is silently broken - a permissions change, a corrupted WAL segment, whatever - I get an e-mail in the morning rather than finding out when I actually need to recover.

Migrating Deno Queues

This is the part that caught me most off-guard. kv.enqueue() and kv.listenQueue() are proprietary to Deno Deploy - they’re not implemented in the open-source denokv binary at all. So self-hosting doesn’t automatically give you queues back.

The repo ships a drop-in replacement in /lib/kv_queue.ts that reimplements the queue API on top of plain KV atomic operations. You import it straight from the Fly app itself (it’s served as a static file via the proxy):

import { KvQueue, KvQueueRunner } from "https://your-app.fly.dev/lib/kv_queue.ts";

const kv = await Deno.openKv("https://your-app.fly.dev");
const queue = new KvQueue(kv);

// enqueue a job (with optional delay)
await queue.enqueue("send-email", { to: "user@example.com" }, { delayMs: 5000 });

// register a handler and start the runner
queue.handle("send-email", async (data) => {
  // your handler logic here
});

await new KvQueueRunner(kv, queue).run();

The mapping from the official API is straightforward:

FeatureOfficial Deno APISelf-hosted KvQueue
ImportBuilt-inimport { KvQueue } from "https://<app>.fly.dev/lib/kv_queue.ts"
Setupconst kv = await Deno.openKv()const queue = new KvQueue(kv)
Enqueuekv.enqueue(data, { delay: 5000 })queue.enqueue("type", data, { delayMs: 5000 })
Listenkv.listenQueue(handler)queue.handle("type", handler) + new KvQueueRunner(kv, queue).run()
RetriesManaged by DenoAutomatic exponential backoff + Dead Letter Queue

One difference worth noting: the self-hosted version is typed by message type - you register separate handlers per type string instead of one global listener, which I actually prefer for anything non-trivial.

Repository:

You can found code and full REAMDE.md at github.com/worotyns/denokv-on-the-fly

This article was updated on kwiecień 30, 2026