Task Queues and Background Jobs
Slow work such as sending email, resizing images or building reports belongs in background jobs, not in the web request. Learn how task queues and workers run jobs, retry them, schedule them and report results.
In a nutshell
- Move anything slow or unreliable out of the request: the user gets a fast response and the job runs on a worker.
- Jobs are triggered by events (user signed up), on a schedule (nightly report) or on demand (export my data).
- Jobs fail: retry with backoff, cap the attempts, and send the rest to a dead-letter queue.
- For long jobs, return a job ID at once and let the client poll for status or receive a notification.
What belongs in a background job
- Sending emails, SMS and push notifications.
- Resizing images, transcoding video, generating PDFs.
- Calling slow or flaky third-party APIs (shipping rates, CRM sync).
- Imports and exports of large files, data clean-ups, reports.
- Anything that may take more than a few hundred milliseconds and does not need to finish before the user sees a response.
A task queue is a message queue plus a convention: each message names a task and its arguments, and a pool of workers runs them. Common tools: Celery (Python), Sidekiq (Ruby), BullMQ (Node.js), Laravel queues, Spring with RabbitMQ or SQS, and cloud services such as Cloud Tasks.
Long jobs: return a job ID
A data export that takes two minutes
All steps The API accepts the job and returns at once. The client checks the status, and downloads the file when it is ready.
- The client asks for an export.
- The API records the job and puts it on the queue.
- It replies immediately with 202 Accepted and a job ID.
- A free worker picks up the job and starts working.
- Meanwhile the client checks the job's status.
- The API reports progress stored by the worker.
- The worker finishes, stores the file and marks the job done.
- The client gets a download link (or an email or push notification).
Diagram as text
- The client asks for an export.
- The API records the job and puts it on the queue.
- It replies immediately with 202 Accepted and a job ID.
- A free worker picks up the job and starts working.
- Meanwhile the client checks the job's status.
- The API reports progress stored by the worker.
- The worker finishes, stores the file and marks the job done.
- The client gets a download link (or an email or push notification).
Retries and dead letters
Workers crash, APIs time out, databases restart. A job runner should retry failed jobs with growing delays (backoff), give up after a limit, and keep the failures somewhere visible.
Run jobs with up to N attempts; jobs that keep failing go to a dead-letter list
import java.util.*;
import java.util.function.Predicate;
class JobRunner {
// handler returns true on success. Returns [done, deadLetters].
static List<List<String>> run(List<String> jobs, Predicate<String> handler, int maxAttempts) {
Deque<String> queue = new ArrayDeque<>(jobs);
Map<String, Integer> attempts = new HashMap<>();
List<String> done = new ArrayList<>();
List<String> dead = new ArrayList<>();
while (!queue.isEmpty()) {
String job = queue.pollFirst();
int n = attempts.merge(job, 1, Integer::sum);
if (handler.test(job)) {
done.add(job);
} else if (n < maxAttempts) {
queue.addLast(job); // retry later (a real runner waits with backoff)
} else {
dead.add(job);
}
}
return List.of(done, dead);
}
}from collections import deque
def run_jobs(jobs, handler, max_attempts):
"""handler(job) returns True on success. Returns (done, dead_letters)."""
queue = deque(jobs)
attempts = {}
done, dead = [], []
while queue:
job = queue.popleft()
attempts[job] = attempts.get(job, 0) + 1
if handler(job):
done.append(job)
elif attempts[job] < max_attempts:
queue.append(job) # retry later (a real runner waits with backoff)
else:
dead.append(job)
return done, deadScheduled jobs
Nightly reports, hourly clean-ups and subscription renewals run on a schedule. With several servers, a plain crontab on each one runs the job several times. Use one scheduler (a cron service, the task queue's scheduler, or a distributed lock so only one instance fires), and make the job safe to run twice anyway.
A cron expression: 02:30 every day (minute, hour, day of month, month, day of week)
30 2 * * * generate-daily-sales-reportCommon mistakes
- Doing slow work inside the web request, so requests time out under load.
- Non-idempotent jobs that double-send or double-charge when retried.
- Retrying forever with no backoff, hammering a service that is already down.
- Running the same cron job on every server.
Interview questions
Try answering out loud before you open each one.
Users upload videos that take minutes to process. How do you design it?
Upload straight to object storage, then enqueue a processing job with the object key and return a video ID right away. Workers transcode, store the outputs and update the status. The client polls the status or receives a notification. Retries handle worker crashes; failures after N attempts go to a dead-letter queue and alert. Scale workers on queue length.
Check yourself
Related topics
Further reading
- Celery: Introduction Celery documentation
- Amazon SQS dead-letter queues AWS documentation
- Asynchronous Request-Reply pattern Microsoft Azure Architecture Center
Your notes
Saved in this browser as you type.
Finished this topic?
Mark it read to track your progress, then carry on with the next one.