Retries & Backoff Cheatsheet
One-screen lookup. Cite: Retrying failing jobs, Stalled Jobs.
Failure triggers
| Trigger | Result |
Processor throws Error | Job → failed, retried if attempts > 1 |
Processor throws non-Error (string/number) | Corrupts bookkeeping — never do this |
Worker doesn't renew lock within stalledInterval | Job → stalled → moved back to waiting |
Stall count exceeds maxStalledCount | Job → failed permanently |
Retry options (on add() or defaultJobOptions)
| Option | Type | Notes |
attempts | number | Total tries incl. first. 3 = 1 + 2 retries. |
backoff.type | fixed | exponential | custom | Omit → instant retry (usually bad). |
backoff.delay | ms | Base delay. |
backoff.jitter | 0–1 | Randomises delay to avoid thundering herd. 0.5 = good default. |
removeOnComplete | number | true | Keep last N, or delete immediately. Required in prod. |
removeOnFail | number | true | Same. Keep enough to debug. |
Backoff math
fixed: delay (constant)
exponential: 2^(attemptsMade - 1) * delay (doubles each time)
jitter: random between (delay * (1-j)) and delay
Example, exponential, delay 1000, jitter 0.5, all failing:
attempt 1 → wait ~500–1000ms
attempt 2 → wait ~1000–2000ms
attempt 3 → wait ~2000–4000ms
attempt 4 → wait ~4000–8000ms
attempt 5 → FAILED (if attempts: 5)
Custom backoff (on Worker, not Queue)
new Worker('q', processor, {
settings: {
backoffStrategy: (attemptsMade, job) => {
const cap = 60_000;
const ms = Math.min(cap, 2 ** (attemptsMade - 1) * 1000);
return ms + Math.random() * 500;
// return 0 → back of waiting list
// return -1 → fail now, no more retries
},
},
});
await queue.add('x', data, { attempts: 8, backoff: { type: 'custom' } });
Dead-letter queue pattern
qe.on('failed', async ({ jobId, failedReason, prev }) => {
if (prev !== 'active') return; // fires every attempt
const job = await queue.getJob(jobId);
if (!job) return;
if (job.attemptsMade >= (job.opts.attempts ?? 1)) {
await dlq.add(job.name, {
originalId: job.id, data: job.data, reason: failedReason,
});
}
});
Stall options (on Worker)
| Option | Default | Notes |
stalledInterval | 30000ms | How often BullMQ checks for stalled jobs. |
maxStalledCount | 1 | After this many stalls → failed. |
Cleanup methods
| Method | Removes |
queue.drain() | waiting + delayed (not active/completed/failed) |
queue.drain(true) | above + delayed jobs explicitly |
queue.clean(graceMs, count, state) | jobs in state older than grace |
queue.obliterate() | everything — queue is gone, no undo |
job.remove() | one specific job |
job.retry() | manually re-queue a failed job |
Inside the processor
| Field | Holds |
job.attemptsMade | 1-based; current attempt number |
job.opts.attempts | configured max attempts |
job.failedReason | message from the last thrown Error |
job.attemptsStarted | incl. stalled restarts (v5+) |