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.