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/healthas “the process answers” and/api/readyas “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
| Environment | Recommended runner |
|---|---|
| Local development | pnpm dev:all starts pnpm jobs:work |
| VM/container/Kubernetes | pnpm jobs:work --production |
| Platform scheduler | pnpm jobs:run --production |
| Vercel | authenticated /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
- Set
CRON_SECRETto at least 32 random bytes. - Confirm the scheduler calls the route every five minutes.
- Watch structured
cron.jobslogs for claimed, retrying, failed, and lease-lost counts. - Alert on a non-200 readiness response and on a growing failed queue.
- 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.
Content Studio and Marketing Email
Run the separate Payload authoring app, keep customer data in the SaaS, and launch consent-aware marketing campaigns safely.
Testing and CI Contracts
Use the starter's test tiers, real infrastructure suite, Playwright flow, coverage ratchet, and build gates without weakening their guarantees.