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:
- 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.
- denokv - waits for the database to be ready, then serves KV-Connect on port 4513.
- deno - proxy to 4512 and serve /lib files.
- Supercronic - runs a daily cron job at 02:00 UTC that restores the latest backup into
/tmp, runsPRAGMA 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:
| Feature | Official Deno API | Self-hosted KvQueue |
|---|---|---|
| Import | Built-in | import { KvQueue } from "https://<app>.fly.dev/lib/kv_queue.ts" |
| Setup | const kv = await Deno.openKv() | const queue = new KvQueue(kv) |
| Enqueue | kv.enqueue(data, { delay: 5000 }) | queue.enqueue("type", data, { delayMs: 5000 }) |
| Listen | kv.listenQueue(handler) | queue.handle("type", handler) + new KvQueueRunner(kv, queue).run() |
| Retries | Managed by Deno | Automatic 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