Skip to content

Reliability

The product-level semantics are described in Retries, recovery & delivery semantics. This page covers how the platform behaves when its own dependencies fail.

Failure matrix

FailureEffectRecovery
API replica crashesIn-flight requests fail; clients retry with the same idempotency keyOther replicas serve; container restarts
WhatsApp worker crashesIts sessions drop; in-flight sends become UNKNOWN (or are re-queued if never attempted)Leases expire after LEASE_TTL_MS; other workers (or the restarted one) claim and restore without QR
Worker deploy (SIGTERM)Sockets closed without logout, leases releasedImmediate takeover by remaining/new workers
Webhook worker downEvents accumulate in the outboxFan-out and delivery resume from the database
Scheduler downExpiry/retention/metrics snapshots pauseJobs resume; nothing is lost
PostgreSQL unavailableAPI returns errors for writes (5xx), readiness fails; workers cannot claim or record; heartbeats failSelf-fencing closes sockets after one TTL; everything resumes from database state when it returns
Redis unavailableCommands and nudges are delayed (workers fall back to polling/claim loop), QR codes cannot be served, rate limiting fails open, BullMQ maintenance pausesReconnects automatically; no durable state lives only in Redis
S3 unavailableMedia uploads and media sends fail (MEDIA_UNAVAILABLE if it persists through retries)—
WhatsApp unreachableSessions go RECONNECTING; messages wait in the queue until reconnect or expiryBackoff reconnect; ERROR after 12 attempts (manual reconnect)
Customer webhook endpoint downDeliveries retry for ~16 h, then deadReplay from dashboard/API; /v1/events for reconciliation

Design rules that make this work

  1. PostgreSQL is the only source of truth. Redis holds caches, nudges, rate-limit buckets and ephemeral QR codes; losing Redis loses nothing that cannot be rebuilt.
  2. No dual writes. The message queue and webhook outbox are tables written in the same transaction as the business change.
  3. Intent in the database, commands as nudges. A lost BullMQ command is corrected by the next reconciliation.
  4. Fencing everywhere it matters. Session key writes and claims are guarded by lease_epoch.
  5. Pessimistic about outcomes. Anything that might have been sent is UNKNOWN, never silently retried.
  6. Bounded everything. Retries (sends: until expiry; webhooks: 8 attempts; reconnects: 12), queue retention in BullMQ, backlog per project, upload sizes, bodies read from customer servers (1 KB preview of webhook responses; media imports capped at the media size limit), HTTP timeouts.

Graceful shutdown

Each process registers ordered shutdown steps (ShutdownManager) with a hard deadline (API: Nest shutdown hooks; workers: 30–60 s) — stop accepting work, drain, close connections. Compose sets stop_grace_period accordingly.

Health checks

  • /health/live: the process is running (no dependency checks).
  • /health/ready: PostgreSQL and Redis respond; for workers also that the ownership/dispatch loop ran recently.
  • A disconnected or restricted WhatsApp account never affects health; instance health is reported through status, events and metrics instead.