background-jobs · git:20260720.1dc980e · 2026-07-20 · sha256 15204ce1319ed83e
background-jobs git:20260720.1dc980eA
Immutable. This exact content is served forever at /api/v1/blob/15204ce1319ed83e.
---
name: background-jobs
description: Run asynchronous work through queues with safe retries, poison-message handling, and idempotent jobs. Use when moving work out of the request path or fixing duplicated, stuck, or lost jobs.
---
# Background jobs
A job system's contract is at-least-once execution at some later time.
Design every job for the "at least" and the "later": it will run twice,
and it will run after the world changed.
## Method
1. **Enqueue references, not state.** The payload is IDs plus intent
(`{"type": "send_receipt", "order_id": 123}`); the job reloads current
state at run time. Snapshotting state into payloads acts on stale data
after any delay or retry.
2. **Make every job idempotent.** Natural idempotency where possible
(setting a status, upserting), otherwise an idempotency key checked in
the job's own transaction. The queue will redeliver after crashes
between work and ack; only the job can make that safe.
3. **Retry with backoff, jitter, and a ceiling.** Exponential backoff
starting at seconds, jittered to avoid thundering herds, capped
attempts (5-10). Distinguish retryable failures (timeouts, 429s) from
permanent ones (validation): permanent failures skip retries and go
straight to the dead-letter queue.
4. **Dead-letter with context, and staff the queue.** Failed-forever jobs
land in a DLQ with error, attempt count, and first-failure time. A DLQ
nobody monitors is a silent data-loss pit: alert on depth and age, and
build the one-click requeue for after the fix ships.
5. **Set visibility timeouts above p99 runtime.** Too short and the queue
redelivers mid-run (guaranteed duplicates); heartbeat/extend for long
jobs. Bound job runtime hard; a job that can run for an hour needs to
be a resumable sequence of smaller jobs.
6. **Separate queues by latency class.** User-facing (email, receipts) on
a fast queue with tight alerting; batch/reporting on another. One queue
means a backfill starves every password-reset email behind it.
7. **Instrument the four numbers.** Queue depth, oldest-message age,
per-type failure rate, per-type duration. Age is the alert that
matters; depth alone lies when throughput is high.
## Boundaries
- Scheduled recurrence (cron) is a different concern; see scheduled-jobs.
A cron that enqueues jobs beats a job that sleeps.
- Exactly-once execution is not purchasable from the queue; it is
idempotency plus at-least-once, end of list (see idempotent-consumers).
- Workflows with dependencies between steps deserve an orchestrator or
the saga-pattern, not job-spawning-job chains you cannot observe.