DelayKit
Durable wake‑ups for TypeScript apps and agents.
Reminders, expirations, retries, debounces, and agent resumes. Backed by Postgres or SQLite.
npm install delaykitbun add delaykit
Code
What it looks like
// when a user signs up, schedule a reminder for 24h later app.post("/signup", async (req, res) => { const user = await createUser(req.body); await dk.schedule("remind-onboarding", { key: user.id, delay: "24h", }); res.json({ ok: true }); }); // the DelayKit handler runs at the scheduled time dk.handle("remind-onboarding", async ({ key }) => { const user = await db.users.find(key); if (user.onboarded) return; await sendEmail(user.email, "ready to finish setup?"); });
Patterns
What you can do with it
Wake an agent after a timeout
Schedule a timeout for an agent run waiting on human input. The handler resumes the run if approval doesn't arrive.
dk.schedule("agent-timeout", { delay: "24h" })cancel when approval arrives
Send a reminder
Schedule a notification for later. The handler checks current state when it fires.
dk.schedule("remind", { delay: "24h" })handler decides whether to send
Expire something
Run a side effect (email, cleanup, notification) when an invitation, checkout hold, or magic link expires. Handler reads current state and skips if the user already acted.
dk.schedule("expire-invitation", { delay: "7d" })handler skips if the user already accepted
Debounce a flurry
Collapse fifty events into one action. Durable across restarts.
dk.debounce("reindex", { wait: "5s" })optional maxWait to cap the window
Properties
What DelayKit handles
- Schedule, debounce, throttle. All per entity, all cancellable. Not just “run this at time X.” Debounce a burst of edits into one reindex. Throttle notifications to one per hour per user. Cancel any of it if it’s no longer needed.
- Postgres or SQLite. Use what fits. SQLite for single-process apps, zero infra. Postgres for multi-replica and serverless. Either store auto-migrates on first connect.
- Jobs survive restarts and deploys. Durable in Postgres or SQLite, not in memory. Crash, redeploy, scale. They’re still there.
- No duplicate pending jobs. Same handler and key won’t queue twice. Safe to call from any request handler.
- Retries built in. Handlers retry on failure with configurable backoff. Stalled jobs from crashed processes recover automatically.
- Zero runtime dependencies.
postgres,better-sqlite3, and@posthook/nodeare optional peers. Install only what your deployment needs.
Stores
Pick a store
- SQLite. Local-first, zero infra. For single-process apps: a Bun server, a Node backend on one VPS, a desktop or CLI tool.
bun:sqliteis built in. On Node, installbetter-sqlite3as an optional peer. - Postgres. Multi-replica. For multi-instance apps and serverless. Share an existing pool or pass a connection string. Works with Neon, Supabase, Railway, or any Postgres.
Runtimes
Pick a runtime
- Long-running process. Node, Bun, Docker, VPS, Fly. Call
dk.start()to poll continuously. Works with SQLite or Postgres. - Serverless and cron. Vercel, Lambda. A cron route calls
dk.poll()on a schedule to drain due jobs. Postgres only. See the deploy guide ↗ - Posthook webhook delivery. Posthook fires each job as a webhook at the scheduled time. No cron, no long-running process.
Boundaries
When not to use it
- setTimeout if the timer fits in one request. Or if losing the timer on restart is acceptable. The standard library is enough.
- A queue for short-lived high-throughput jobs. BullMQ and friends are Redis-backed and tuned for that shape. DelayKit composes cleanly with one: schedule with DelayKit, enqueue from the handler.
- A workflow engine for multi-step pipelines. Inngest and Temporal track state across steps, branch on outcomes, and retry the whole chain when a step fails. DelayKit handles durable waits, but the multi-step flow itself is DIY.