Skip to content

ADR 0022: BullMQ for all async jobs

  • Status: Accepted
  • Date: 2026-04-14
  • Deciders: Ideony team

The API needs async job execution for multiple use cases: notification fan-out (Novu → email/SMS/push), webhook retry on transient failures, scheduled reminders (booking follow-ups, service due reminders), and deferred work triggered by state transitions (booking accept → notify pro, SOS accept → cascade halt). “Fire-and-forget from the controller” was considered but loses durability guarantees — a service crash mid-job means silent data loss.

Use BullMQ for every async / deferred / retryable job. BullMQ workers run in the same NestJS process as the API (single-container deploy — ADR-0012). Redis 8.6 (already present for caching — ADR-0013) backs the queue.

  • Durable job state — crashes don’t drop work; exponential backoff retries built-in
  • Unified async substrate — same pattern for notifications, webhooks, reminders (DRY)
  • Redis already in the stack — zero incremental infra
  • Bull Board UI available for ops visibility if needed later − Redis becomes a load-bearing dependency (mitigated by ADR-0013’s noeviction policy + future HA plan) − Queue schema drift risk — enforce typed payloads (Zod — ADR-0016)
  • Raw setImmediate + fire-and-forget — considered; rejected because non-durable, no retry, no visibility
  • pg-boss (Postgres-backed queue) — fewer dependencies but worse throughput + no priority queues + less tooling
  • Temporal / Inngest — richer workflow features but adds external service, cost, and complexity; overkill for MVP 0
  • AWS SQS / Cloud Tasks — vendor lock-in + egress cost; conflicts with Hetzner + Dokploy infra