Retries & Backoff Cheatsheet

One-screen lookup. Cite: Retrying failing jobs, Stalled Jobs.

Failure triggers

TriggerResult
Processor throws ErrorJob → failed, retried if attempts > 1
Processor throws non-Error (string/number)Corrupts bookkeeping — never do this
Worker doesn't renew lock within stalledIntervalJob → stalled → moved back to waiting
Stall count exceeds maxStalledCountJob → failed permanently

Retry options (on add() or defaultJobOptions)

OptionTypeNotes
attemptsnumberTotal tries incl. first. 3 = 1 + 2 retries.
backoff.typefixed | exponential | customOmit → instant retry (usually bad).
backoff.delaymsBase delay.
backoff.jitter0–1Randomises delay to avoid thundering herd. 0.5 = good default.
removeOnCompletenumber | trueKeep last N, or delete immediately. Required in prod.
removeOnFailnumber | trueSame. 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)

OptionDefaultNotes
stalledInterval30000msHow often BullMQ checks for stalled jobs.
maxStalledCount1After this many stalls → failed.
Stalls usually mean CPU-bound work blocking the event loop. Move it to a sandboxed processor.

Cleanup methods

MethodRemoves
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

FieldHolds
job.attemptsMade1-based; current attempt number
job.opts.attemptsconfigured max attempts
job.failedReasonmessage from the last thrown Error
job.attemptsStartedincl. stalled restarts (v5+)