Operate and launch

Durable Jobs and Readiness

Operate the PostgreSQL job queue, cron runner, retries, deduplication, liveness and dependency-readiness probes.

Synced with starter commit 2a1a04a.

Use this page when an action must finish even after the browser request ends—for example sending mail, granting credits, deleting an object, or completing a privacy request. The starter ships a small PostgreSQL-backed queue, so most teams can launch without operating Redis queues or a separate worker service.

Decide how you will run and monitor background work

  • On Vercel, keep the authenticated cron drain. On a VM, container, or Kubernetes, run the shipped dedicated worker instead of inventing another queue loop.
  • Keep the PostgreSQL queue while throughput is modest. The portable runner and cron share the same claim, retry, timeout, maintenance, and observability services.
  • Treat /api/health as “the process answers” and /api/ready as “this release can receive production traffic.” They answer different operational questions.

Ready to launch means: a test job is claimed, a forced failure retries and eventually surfaces, overlapping cron calls do not duplicate the effect, and your monitor alerts on readiness failures and a growing failed queue.

Why Work Is Persisted

Serverless instances may freeze as soon as a response is returned. Work that must happen—email delivery, credit grants, object deletion, account export, and erasure—is written to the jobs table instead of being left in queueMicrotask or an un-awaited promise.

Current job types also include marketing delivery and the five-credit image workflow, alongside email, signup credits, Slack, storage deletion, reservations, exports, and erasure.

Enqueue Safely

await enqueueJob("welcome_email", { email, name, userUuid }, {
  dedupeKey: `welcome:${userUuid}`,
  subjectUserUuid: userUuid
});

dedupeKey makes enqueueing idempotent. Subject UUIDs let privacy workflows cancel or scrub queued work. Handlers must also be idempotent because a provider may accept a request before the worker sees its response.

Choose one default runner

EnvironmentRecommended runner
Local developmentpnpm dev:all starts pnpm jobs:work
VM/container/Kubernetespnpm jobs:work --production
Platform schedulerpnpm jobs:run --production
Vercelauthenticated /api/cron/jobs

The admin /jobs page exposes status, attempts, due age, subject, and safe error detail without payload JSON. admin_rw operators can retry failed idempotent jobs or cancel standalone pending notifications with a required audit note. Domain-owned jobs—credits, storage, lifecycle, campaigns—must be canceled through their owning workflow.

Runner Behavior

GET /api/cron/jobs is protected by Authorization: Bearer $CRON_SECRET. vercel.json invokes it every five minutes. Outside Vercel, schedule the same authenticated request yourself.

The runner:

  • claims one due job at a time with FOR UPDATE SKIP LOCKED;
  • allows overlapping cron calls without double execution;
  • uses a five-minute lease and reclaims stale workers;
  • limits each handler to 20 seconds and the drain to 40 seconds;
  • retries up to five times by default with exponential backoff from 30 seconds;
  • keeps succeeded/failed jobs for 14 days;
  • also cleans stale upload reservations and sweeps stuck Stripe events.

Never expose the cron route publicly without CRON_SECRET, and generate a different secret from BETTER_AUTH_SECRET.

Liveness vs Readiness

GET or HEAD /api/health is a cheap process liveness check. It does not touch dependencies.

GET /api/ready verifies:

  • production environment configuration;
  • database access and compiled migration availability;
  • Redis-backed distributed rate limiting;
  • queue health, including stale running or failed jobs.

Missing database, migrations, Redis, or required environment configuration returns 503. Queue failures mark the report degraded but do not remove the web application from service.

Use /api/health for a platform's frequent liveness probe and /api/ready before routing production traffic or completing a deployment.

Operational Checks

  1. Set CRON_SECRET to at least 32 random bytes.
  2. Confirm the scheduler calls the route every five minutes.
  3. Watch structured cron.jobs logs for claimed, retrying, failed, and lease-lost counts.
  4. Alert on a non-200 readiness response and on a growing failed queue.
  5. Keep handlers additive and payload changes backward compatible; queued JSON may outlive the deploy that created it.

Next: Carry these runner, scheduler, and readiness checks into Deployment and Security before sending production traffic.

Durable Jobs and Readiness · Sushi SaaS