When a job fails, Spooled automatically schedules a retry with exponential backoff.
This prevents overwhelming downstream services and gives transient failures time to resolve.
Processing fails (exception, timeout, or explicit failure)
Spooled checks if retry attempts remain
If retries remain: the job waits as scheduled until its run time, then becomes pending
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.
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 queuecurl -X GET "https://api.spooled.cloud/api/v1/jobs/dlq?queue_name=my-queue&limit=100" \ -H "Authorization: Bearer sp_live_YOUR_API_KEY"
// List jobs in dead-letter queueconst dlqJobs = await client.jobs.dlq.list({ queueName: 'payment-processing', limit: 100,});for (const job of dlqJobs) { console.log(`DLQ Job: ${job.id}, status: ${job.status}`); console.log(`Attempt: ${job.attempt}, created: ${job.createdAt}`);}
# List jobs in dead-letter queuedlq_jobs = client.jobs.dlq.list({ "queue_name": "payment-processing", "limit": 100})for job in dlq_jobs: print(f"DLQ Job: {job.id}, status: {job.status}") print(f"Retry count: {job.retry_count}, created: {job.created_at}")
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.
• 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.statusdeadletter 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" }'
// Fail the job (will retry with exponential backoff)await client.jobs.fail(job.id, { workerId: 'worker-1', leaseId: job.leaseId ?? undefined, error: 'Connection timeout to downstream service',});
# Fail the job (will retry with exponential backoff)client.jobs.fail(job.id, { "worker_id": "worker-1", "lease_id": job.lease_id, "error": "Connection timeout to downstream service"})
We use analytics cookies (Google Analytics, Meta Pixel) to understand how the site is
used. You can decline at any time.Nothing is collected until you agree.
See our
privacy policy
.