DataQueueDataQueue
Usage

Failed Jobs

A job handler can fail for many reasons, such as a bug in the code or running out of resources.

When a job fails, it is marked as failed and retried up to maxAttempts times (default: 3). You can view the error history for a job in its errorHistory field.

Dead-letter queues

You can route permanently failed jobs to a dead-letter job type using deadLetterJobType.

When a job exhausts retries (attempts >= maxAttempts), DataQueue:

  1. Keeps the source job as failed.
  2. Creates a new pending dead-letter job in deadLetterJobType.
  3. Stores linkage metadata on the source job (deadLetteredAt, deadLetterJobId).
await jobQueue.addJob({
  jobType: 'email',
  payload: { to: 'user@example.com' },
  maxAttempts: 3,
  deadLetterJobType: 'email_dead_letter',
});

The dead-letter job payload is an envelope:

{
  originalJob: { id, jobType, attempts, maxAttempts },
  originalPayload: { ... }, // original job payload
  failure: { message, reason, failedAt },
}

If deadLetterJobType is not set, behavior is unchanged: exhausted jobs remain failed without creating a dead-letter job.

Retry configuration

You can control the retry behavior per-job using three options:

OptionTypeDefaultDescription
retryDelaynumber60Base delay between retries in seconds
retryBackoffbooleantrueUse exponential backoff (doubles each attempt)
retryDelayMaxnumbernoneMaximum cap for the delay in seconds

Fixed delay

Set retryBackoff: false to use a constant delay between retries:

await jobQueue.addJob({
  jobType: 'email',
  payload: { to: 'user@example.com' },
  maxAttempts: 5,
  retryDelay: 30, // 30 seconds between each retry
  retryBackoff: false,
});

Every retry will wait exactly 30 seconds.

Exponential backoff (default)

When retryBackoff is true (the default), the delay doubles with each attempt. A small amount of random jitter is added to prevent thundering herd problems:

await jobQueue.addJob({
  jobType: 'email',
  payload: { to: 'user@example.com' },
  maxAttempts: 5,
  retryDelay: 10, // base: 10 seconds
  retryBackoff: true, // enabled by default
});

This produces approximate delays of 10s, 20s, 40s, 80s, ... (with jitter).

Capping the delay

Use retryDelayMax to prevent the delay from growing unbounded:

await jobQueue.addJob({
  jobType: 'email',
  payload: { to: 'user@example.com' },
  maxAttempts: 10,
  retryDelay: 5,
  retryBackoff: true,
  retryDelayMax: 300, // never wait more than 5 minutes
});

Delays: ~5s, ~10s, ~20s, ~40s, ~80s, ~160s, ~300s, ~300s, ...

Default behavior

If none of the retry options are set, the legacy formula 2^attempts * 1 minute is used. This means the first retry is after ~2 minutes, then ~4 minutes, then ~8 minutes, and so on.

Jitter

When exponential backoff is enabled, each computed delay is multiplied by a random factor between 0.5 and 1.0. This prevents multiple failed jobs from retrying at exactly the same time, which could overload downstream services.

Cron schedules

Retry configuration can also be set on cron schedules. Every job enqueued by the schedule inherits the retry settings:

await jobQueue.addCronJob({
  scheduleName: 'daily-report',
  cronExpression: '0 9 * * *',
  jobType: 'report',
  payload: { type: 'daily' },
  retryDelay: 60,
  retryBackoff: true,
  retryDelayMax: 600,
});

Editing retry config

You can update the retry configuration of a pending job:

await jobQueue.editJob(jobId, {
  retryDelay: 15,
  retryBackoff: false,
});