DelayKit
The timing layer for Next.js.
Remind users who haven’t activated. Expire trials. Reindex once after edits settle.
npm install delaykit
// handler checks state, retries on failure dk.handle("remind", { handler: async ({ key }) => { const user = await db.users.find(key); if (user.onboarded) return; await sendEmail(user.email, "heads up"); }, retry: { attempts: 5, backoff: "exponential", }, }); // schedule it for 24 hours from now await dk.schedule("remind", { key: "user_123", delay: "24h", });
Patterns
What you can do with it
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 cleanup the moment a deadline passes. Invitations, trials, holds.
dk.schedule("expire-trial", { delay: "14d" })handler skips if already resolved
Debounce a flurry
Collapse fifty events into one action. Durable across restarts.
dk.debounce("reindex", { wait: "5s" })optional maxWait to cap the window
Retry with backoff
Something failed during a request. Defer the retry instead of blocking the response.
dk.schedule("retry-charge", { delay: "1m" })handler retries with configurable backoff
Renew before expiry
Refresh a token, lease, or session a few minutes before it expires.
dk.schedule("refresh", { delay: "55m" })reschedule on each successful renewal
Dead man's switch
Alarm when something stops happening. The absence of an event is the trigger.
dk.schedule("missed-heartbeat", { delay: "5m" })cancel on each ping, fires only on silence
Properties
The shape of it
- 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.
- Uses the Postgres you already have. No Redis, no managed queue. DelayKit creates its own table and handles migrations automatically.
- Jobs survive restarts and deploys. Postgres-backed, not memory. Crash, redeploy, scale. They’re still there.
- No duplicate pending jobs. Same handler + key won’t queue twice. Safe to call from any request handler.
- Retries built in. Handlers retry on failure with configurable backoff. No extra wiring.
- Works in dev and on Vercel. PollingScheduler locally, Posthook or Vercel Cron in production. Same handlers, same store.
Deploy
Works on Vercel
- Vercel + Posthook — managed delivery. Posthook fires each job as a webhook at the scheduled time. No cron route, no long-running process.
- Vercel + cron — self-hosted polling. A Vercel Cron route calls
dk.poll()on a schedule to drain due jobs, well inside the 10s function limit. No external scheduler required. See the deploy guide ↗ - Auto-migrates on first connect. Works with Neon, Supabase, Railway — any Postgres. On a long-running server, call
dk.start()instead ofdk.poll().