Background Jobs & Scheduling

How scheduled (cron) and asynchronous background work runs on the platform

The platform runs scheduled tasks (cron) and asynchronous background work on BullMQ, backed by a shared Redis instance. Work that shouldn't block an HTTP response — email delivery, AI calls, webhook fan-out, file processing, periodic maintenance — is handed to a queue and processed by a dedicated worker.

Web pods vs. worker pods

Background jobs do not run inside the request-serving (web) pods. A WORKER_ROLE environment variable decides what a process boots:

WORKER_ROLE What it runs Where
web (default) HTTP only — no workers the web Deployment
worker cron + async job handlers, in one process the worker Deployment

Because WORKER_ROLE defaults to web, the web pods never accidentally start processing jobs. The cron and async handlers can later be split into separate Deployments if their scaling needs diverge, without any code change.

graph LR
    Web[Web pods
WORKER_ROLE=web] -- enqueue --> Redis[(Redis
BullMQ)] Redis -- dequeue --> Worker[Worker pods
WORKER_ROLE=worker] Worker -- cron + async handlers --> DB[(Database / external APIs)]

Two kinds of work

  • Static cron — schedules known at build time (e.g. "renew expiring mail subscriptions every hour"). Registered in code at worker boot; the schedule is reconciled automatically when it changes.
  • Async queues — work triggered by a request that the user shouldn't wait for. The request handler enqueues a job and returns immediately; a worker picks it up.

Idempotency is mandatory

A job may run more than once for the same input — BullMQ retries failed jobs, recovers jobs from a crashed worker, and supports manual re-enqueue. Every handler must therefore be idempotent: running it twice with the same payload must be safe. Common patterns are compare-and-set state transitions, idempotency keys, and naturally convergent ("renew if expiring, else no-op") operations.

For contributors

Step-by-step guidance for adding a cron or async job — local setup (npm run redis, npm run dev:worker*), the handler registry, connection rules, and the idempotency contract — lives in the internal developer docs at docs/development/queue-and-cron.md. The shared cluster-level design (Redis topology, network policy, per-app worker Deployments) is documented in the platform-infra repository.