Skip to content

Retries and failures

Retries are part of the Job product. A task handler can throw when work fails. Job records the failed attempt, chooses the next delay, and asks Queue to deliver another attempt until the run succeeds or exhausts attempts.

Configure defaults on the Job instance:

Terminal window
const jobs = job({
name: "background",
retry: {
attempts: 3,
backoff: "exponential",
initialDelaySeconds: 5,
maxDelaySeconds: 300,
},
})

Every task inherits this policy.

Override retry policy for a task:

Terminal window
jobs.task("send-email", {
retry: {
attempts: 5,
backoff: "fixed",
initialDelaySeconds: 30,
},
}, async (payload) => {
await sendEmail(payload)
})

Webhook delivery often needs specific retry intervals. Use custom delays when attempt spacing should follow an explicit sequence:

Terminal window
jobs.task("deliver-webhook", {
retry: {
attempts: 8,
backoff: {
type: "custom",
delaysSeconds: [
10,
30,
120,
300,
900,
3600,
21600,
],
},
},
}, async (payload) => {
await deliverWebhook(payload)
})

The first failed attempt uses the first delay. When all attempts are exhausted, the run becomes dead_lettered.

Terminal window
attempt starts
-> handler throws
-> attempt is marked failed
-> run is marked retrying
-> Queue retries after the selected delay

When the final attempt fails:

Terminal window
attempt starts
-> handler throws
-> attempt is marked failed
-> run is marked dead_lettered
-> message is acknowledged

Dead-lettered runs remain queryable:

Terminal window
const failed = await jobs.runs.list({
status: "dead_lettered",
taskName: "deliver-webhook",
})

Replay creates a new run with the same task and payload:

Terminal window
await jobs.runs.replay(runId, {
reason: "endpoint recovered",
})

Use replay after fixing a handler, restoring a provider, or updating the destination endpoint.

Terminal window
await jobs.runs.cancel(runId)

Cancellation marks the run as canceled. A Queue delivery that later wakes up for a canceled run is acknowledged without executing the task handler.

Task handlers should tolerate retries. Use stable business IDs and idempotency keys when the task performs side effects:

Terminal window
jobs.task("charge-card", {}, async (payload) => {
await payments.charge({
customerId: payload.customerId,
amount: payload.amount,
idempotencyKey: payload.paymentAttemptId,
})
})