Skip to content
On this page

Retries & Dead-Letter Queue

Job failures are inevitable. Spooled provides automatic retries with exponential backoff and a dead-letter queue for jobs that can't be processed.

How Retries Work

When a job fails, Spooled automatically schedules a retry with exponential backoff. This prevents overwhelming downstream services and gives transient failures time to resolve.

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#fef3c7', 'primaryTextColor': '#92400e', 'primaryBorderColor': '#f59e0b', 'lineColor': '#6b7280'}}}%%
stateDiagram-v2
    [*] --> pending
    pending --> claimed: Worker claims
    claimed --> completed: Success
    claimed --> failed: Error
    failed --> scheduled: Retry (attempts left)
    scheduled --> pending: Run time reached
    failed --> dlq: Max retries exceeded
    dlq --> pending: Manual replay
    completed --> [*]

Retry Flow

  1. Worker claims a job and attempts to process it
  2. Processing fails (exception, timeout, or explicit failure)
  3. Spooled checks if retry attempts remain
  4. If retries remain: the job waits as scheduled until its run time, then becomes pending
  5. If max retries exceeded: job moves to dead-letter queue (DLQ)

Backoff

Retries use exponential backoff with a small random jitter, which spreads retries out and prevents thundering-herd problems. The delay doubles with every retry and is capped at 60 seconds: min(2^retries_so_far, 60) seconds plus 0–500 ms of jitter.

Retry Delay before it runs Time since first failure
1 1s ~1s
2 2s ~3s
3 4s ~7s
4 8s ~15s
5 16s ~31s
6 32s ~1 min
7 60s (cap) ~2 min
8 60s ~3 min
9 60s ~4 min
10 60s ~5 min

The delay is when the job becomes claimable again; a worker picks it up on its next poll, so observed gaps are slightly longer. With the default max_retries: 3, a job that keeps failing reaches the DLQ about 7 seconds after its first failure.

The backoff curve is fixed server-side — there is no per-job strategy, base or cap to set. If a job needs a longer wait (for example, a downstream outage measured in hours), have the worker complete it and enqueue a follow-up job with scheduled_at set to when it should run.

Retry Configuration

Configure retry behavior when creating jobs:

Job with retry configuration
curl -X POST https://api.spooled.cloud/api/v1/jobs \
  -H "Authorization: Bearer sp_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "queue_name": "my-queue",
    "payload": {
      "event": "user.created",
      "user_id": "usr_123",
      "email": "alice@example.com"
    },
    "idempotency_key": "user-created-usr_123"
  }'

Configuration Options

Option Type Default Description
max_retries number settings default (typically 3) Maximum retry attempts before moving to DLQ. Omitted fields use server queue defaults. REST allows explicit 0; gRPC maps <=0 to the settings default.
timeout_seconds number settings default (typically 300) Stored with the job but not enforced by the server or the SDK workers; enforce a time limit in your handler. Re-delivery after a crash is governed by the claim lease, not this field.
priority number 0 Job priority (-100 to 100)

Dead-Letter Queue (DLQ)

Jobs that exhaust all retry attempts land in the dead-letter queue. The DLQ preserves the full job payload and failure history for debugging and replay.

Dashboard Tip

Dashboard → Jobs → Dead Letter Queue

What to look for:

  • → Job payload and metadata
  • → Last error message
  • → Retry count and failure history
  • → Original queue name

Actions:

  • Retry individual jobs
  • Bulk retry all DLQ jobs
  • Purge old failed jobs

List DLQ Jobs

# List jobs in dead-letter queue
curl -X GET "https://api.spooled.cloud/api/v1/jobs/dlq?queue_name=my-queue&limit=100" \
  -H "Authorization: Bearer sp_live_YOUR_API_KEY"

Retry DLQ Jobs

# Retry jobs from DLQ
curl -X POST https://api.spooled.cloud/api/v1/jobs/dlq/retry \
  -H "Authorization: Bearer sp_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "queue_name": "my-queue",
    "limit": 50
  }'

A retried job — whether through POST /api/v1/jobs/dlq/retry or POST /api/v1/jobs/{id}/retry — goes back to pending with retry_count reset to 0, so it gets its full max_retries budget again.

A retry sends a job.retried webhook event when you subscribe to it (data.source says whether it came from job_retry, dlq_retry or workflow_retry). To see what happened to a job before it landed here, read its history: retry_scheduled and deadlettered events carry the error and the attempt.

Purge DLQ

# Purge DLQ jobs (requires confirm: true)
curl -X POST https://api.spooled.cloud/api/v1/jobs/dlq/purge \
  -H "Authorization: Bearer sp_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "queue_name": "my-queue",
    "confirm": true
  }'

DLQ Best Practices

  • • Get told about every dead-lettered job with an outgoing webhook on job.deadlettered (optionally in Slack, Discord or Teams format). It fires whichever way the job got there: a failure your worker reported, a lease that expired on the last retry, or a worker that went offline. job.failed with data.status deadletter fires for the same jobs, so subscribe to one of the two. There is no built-in DLQ alert beyond that, so also poll GET /api/v1/jobs/dlq or watch the dashboard
  • • Review DLQ jobs regularly (daily for critical queues)
  • • Fix the root cause before replaying jobs
  • • Consider archiving old DLQ jobs to cold storage

Handling Failures in Workers

Workers should explicitly mark jobs as failed with a reason. This helps with debugging and determines retry behavior.

Failing a job
# Fail a job (will retry if retries remaining; echo lease_id from claim)
curl -X POST https://api.spooled.cloud/api/v1/jobs/job_xyz123/fail \
  -H "Authorization: Bearer sp_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "worker_id": "worker-1",
    "lease_id": "lease_abc",
    "error": "Connection timeout"
  }'

Debug Failed Jobs

Dashboard → Jobs → Failed

What to look for:

  • → Error message in last_error field
  • → Retry count vs max_retries
  • → Job payload for invalid data
  • → Timestamps to correlate with logs

Actions:

  • Check worker logs for stack traces
  • Verify external service availability
  • Test payload manually

Next Steps

Last updated 2026-10-16