Skip to content

Expert · Performance and reliability · topic 41 of 54 · 30 min

Background jobs and queues

Explanation

Slow work like sending emails, making PDFs or resizing photos should not happen while the user waits for a response. Instead, the app puts a job in a queue and answers at once; a separate worker program picks up jobs and does them. Good job systems retry failures with growing waits, make repeated jobs safe with idempotency keys, and park jobs that keep failing in a dead-letter queue.

Not everything belongs in the request

A request should finish fast, ideally in well under a second. Some work is slow or depends on other services that may be down. If you do it inside the request, the user waits, and if it fails, the whole request fails.

  • Sending a welcome email or an invoice email
  • Making a PDF ticket or a salary slip
  • Resizing and compressing uploaded photos
  • Building a monthly sales report
  • Calling a slow partner API, like a courier tracking service
Think of it like this: At a bank branch you take a token from the machine, then sit down. The token machine is the queue. The counters are workers: each calls the next token number when it is free. You are not kept standing at the door, and if more people come, the bank can open another counter.

Queues, producers and workers

A queue is a list of jobs waiting to be done, in order. A job is a small message that says what to do, like { type: "welcome-email", userId: 42 }. The producer is the code that adds jobs (usually your API). The consumer, or worker, is a separate process that takes jobs from the queue and does them. You can run more workers when there is more work.

javascript
import { Queue, Worker } from 'bullmq';
const connection = { host: 'localhost', port: 6379 };

// Producer: inside the signup API
const emails = new Queue('emails', { connection });
await emails.add('welcome', { userId: 42 }, {
  attempts: 5,
  backoff: { type: 'exponential', delay: 1000 },
});

// Worker: a separate process
new Worker('emails', async (job) => {
  await sendWelcomeEmail(job.data.userId);
}, { connection });
A queue and a worker with BullMQ, which stores jobs in Redis (runs on a server with Node.js).

Retries and exponential backoff

Jobs fail for temporary reasons: the email service is busy, the network blinked. So workers retry. Exponential backoff means each wait is double the one before: 1 second, 2, 4, 8, and so on, up to a maximum. This gives the other service time to recover instead of hammering it. Many systems also add a little random "jitter" so thousands of jobs do not retry at the same moment.

Attempt that failedWait before next try (base 1 s)
11 second
22 seconds
34 seconds
48 seconds
10capped, for example at 60 seconds

Retries mean a job can run more than once. An idempotency key is a unique id for one piece of work, like "invoice-email:order-9001". Before doing the work, the worker checks if that key is already done. If it is, it skips. Then a retry never sends two invoices or charges a card twice.

A dead-letter queue (DLQ) is a separate queue for jobs that failed every attempt. They are kept there, not lost, so a developer can see the error, fix the cause and run them again.

Scheduled jobs

Some jobs run on a timetable, not because of a request: clean old sessions every night, send EMI reminders every morning, build a report every Monday. These are scheduled jobs, often written with cron syntax: five fields for minute, hour, day of month, month and day of week.

output
0 2 * * *      every day at 02:00
*/15 * * * *   every 15 minutes
0 9 * * 1      every Monday at 09:00
Cron expressions: minute, hour, day of month, month, day of week.
Careful: Check the time zone of your scheduler. Many servers use UTC, which is 5 hours 30 minutes behind IST. A job meant for 9 am in India written as 0 9 * * * on a UTC server runs at 2:30 pm IST.
  • BullMQ: a popular Node.js job queue that uses Redis.
  • RabbitMQ: a message broker (a server whose job is passing messages between programs).
  • Amazon SQS: a managed queue service on AWS, with built-in dead-letter queues.
  • Celery: the common choice for background jobs in Python and Django apps.
Tip: Keep job data small: store ids, not whole objects. The worker loads fresh data from the database when it runs, so it never works on an old copy.

Watch the lesson

Background jobs and queues · 1/7

Emails PDFs Images Reports

Some work is too slow to do while the user waits: emails, PDFs, photo resizing, reports.

Prefer a video by a teacher? Find free videos on “message queue background jobs workers explained” on YouTube ↗

In real life: A train ticket PDF and SMS

When you book a train ticket, the payment and seat booking happen inside the request, because you must know at once if you got the seat. The PDF ticket, the confirmation email and the SMS are background jobs. The booking API adds three jobs to a queue and replies "Booked" in under a second. Workers make the PDF and send the messages. If the SMS provider is down for two minutes, the SMS job retries with backoff and goes through later. An idempotency key per booking makes sure a retry never sends you the SMS twice.

Practice (0/3 done)

Task 1 · Code · Javascript

Write backoff(attempt) that returns how many milliseconds to wait after a failed attempt: 1000 * 2^(attempt - 1), but never more than 60000. So attempt 1 gives 1000, attempt 2 gives 2000, attempt 3 gives 4000.

Task 2 · Code · Javascript

Write async runQueue(jobs, handler, maxAttempts). For each job (in order) call await handler(job). If it throws, try again, up to maxAttempts tries in total. Return { done, failed }: done is the list of job ids that worked; failed is a list of { id, attempts, error } (error is the last error message) for jobs that failed every try. Do not actually wait between tries.

Task 3 · Practice

An online clothes shop does everything inside the "Place order" request: save order, charge card, send email, send SMS, make invoice PDF, update the sales dashboard. Redesign it: what stays in the request and what becomes a background job? Add retry, idempotency and scheduling details.

Quick quiz

Question 1

1.Which task is the best fit for a background job?

Question 2

2.In a queue system, what is a worker?

Question 3

3.With exponential backoff and a 1 second base, how long is the wait after the 3rd failure?

Question 4

4.Why use an idempotency key for a "send invoice" job?

Question 5

5.What is a dead-letter queue?

Read more (free)

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

← Previous: Caching and Redis