tools.queue when needed.
Why
BullMQ job schedulers (v5.16+) replace the deprecated repeatable-jobs API. Cron metadata lives in Redis, aligned with the existing BullMQ stack. Each tick runs the schedule handler withtools; handlers enqueue via tools.queue when needed. Retries and DLQs remain on business queues (BullMQ or SQS).
Setup
- Ensure
"tooling-scheduler-bullmq": "workspace:*"inapps/backend/package.json. - Run
make deps-install. - Set
tools.schedulerin env configs (see table and example below). - Optional dedicated Redis:
SCHEDULER_REDIS_URL=redis://scheduler:6379in.env.development(falls back toREDIS_URLviaresolveSchedulerRedisUrlinapps/backend/src/configuration/resolveSchedulerRedisUrl.ts). - Start one consumer:
make start(Composeconsumerservice). - Run
make test module=tooling-scheduler-bullmq.
Config options (tools.scheduler, BullMQ)
Each schedule
name must match a handler in scheduleRegistry.ts (see Scheduler overview). Omit a name from the array to stop it; reconcile removes orphans on consumer restart.
Example (apps/backend/src/configuration/development.ts):
How the client is created
buildScheduler in apps/tools/scheduler/bullmq/ opens a Redis connection from required connection. On registerSchedules:
- Lists job schedulers on
schedulerQueueNameviagetJobSchedulers. - Removes schedulers not listed in
tools.scheduler.schedules(orphans and stale keys). - For each config entry, calls
upsertJobSchedulerwhen missing or whenpattern/tzchanged (stable scheduler id:scheduler:${scheduleName}).
schedulerTickJobName as the BullMQ job template name; payload includes { scheduleName } so the worker resolves the handler by schedule name, not by job name.
A Worker on the same queue processes ticks: handler → optional tools.queue.sendToQueue.
Official reference: BullMQ job schedulers.
Mixed stack with SQS queue
tools.queue.client may be sqs while tools.scheduler.client is bullmq. Scheduler Redis is separate from SQS; ticks still call tools.queue.sendToQueue, which delivers to SQS (including DelaySeconds cap for deferred sends). See Scheduler overview and Queue overview.
Observability
Implementation:apps/tools/scheduler/bullmq/src/observability.ts. Each registerSchedules run logs a reconcile summary (removed, registered, updated, unchanged) and emits scheduler.reconcile.* metrics. Each tick logs start/completion with durationMs and emits scheduler.tick.* metrics. Failures use categorical reason tags — not raw error text.
Consumer bootstrap logs via scheduler-consumer when apps/backend/src/consumer.ts calls registerSchedules. See the monitoring table in Scheduler overview.
Local observability
If scheduler Redis is the same instance as the queue (REDIS_URL), Bull Board may show scheduler_ticks_v1 alongside business queues. Ticks are lightweight; monitor downstream queue depth for real work.
Production verification
Production and staging useschedules: [] until real cron work is added. After deploy:
- Restart one consumer replica.
- Confirm
scheduler-bullmq-reconcilelogs show no errors;removedmay be1on the first restart if a prior scheduler entry existed in Redis, then0on later restarts. - Confirm
registerSchedulesreturns success in consumer bootstrap logs. - Optional: Bull Board —
scheduler_ticks_v1has no delayed jobs whenschedules: [].
Caveats and limitations
- Single consumer replica — mandatory; no horizontal scaling.
- Redis required — schedules do not run without scheduler Redis.
- No backfill — long outages do not replay every missed cron window automatically.
- 6-field patterns only — standard subset documented in
cronPatternSchema; noL/W/#in v1. - Deploy/restart — pattern changes apply on next
registerSchedules(consumer restart). - Empty
schedules[]— valid; reconcile clears all job schedulers from Redis until entries are added. Production and staging currently use an empty array.
What’s next?
- Scheduler overview — handlers, registry, adding schedules.
- Test scheduler client —
runScheduleNowfor Jest only. - Queue BullMQ — business queue client.
- Configuration
