Ecommet
Topic 41 of 75

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.

Beginner 5 min read 3 quiz questions

Jump to the quiz

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

A data export that takes two minutes Client → API: POST /exports API → Task queue: enqueue export job 77 API → Client: 202 Accepted, job 77 Task queue → Worker: job 77 Client → API: GET /exports/77 API → Client: status: running 40% Worker → API: done: file key API → Client: ready: download link ClientAPITask queueWorkerPOST /exportsenqueue export job 77202 Accepted, job 77job 77GET /exports/77status: running 40%done: file keyready: download link

All steps The API accepts the job and returns at once. The client checks the status, and downloads the file when it is ready.

Diagram as text
  1. The client asks for an export.
  2. The API records the job and puts it on the queue.
  3. It replies immediately with 202 Accepted and a job ID.
  4. A free worker picks up the job and starts working.
  5. Meanwhile the client checks the job's status.
  6. The API reports progress stored by the worker.
  7. The worker finishes, stores the file and marks the job done.
  8. 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

Java
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);
    }
}
Python
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, dead

Scheduled 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)

Example
30 2 * * *   generate-daily-sales-report

Common 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

1. Which should run as a background job?
Show the answer

Answer: Generating a 500-page PDF report. It is slow and the user does not need to wait for it.

2. What should the API return for a job that takes minutes?
Show the answer

Answer: 202 Accepted with a job ID. The client can check the job status later.

3. Why must jobs be idempotent?
Show the answer

Answer: Because they may be retried and run more than once. Retries after crashes and timeouts mean repeats.

Further reading

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.

Next topic