Title: Background Jobs & Scheduling
Description: 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.