System Design

Background Jobs Explained

Learn how background jobs work in backend systems, including queues, workers, retries, scheduling, idempotency, and monitoring.

Quick answer

A background job is work that runs outside the user-facing request path. Instead of making a user wait for slow or retryable work, the backend records a job and processes it later with a worker.

Background jobs are useful for emails, imports, exports, image processing, billing syncs, webhooks, reports, cleanup tasks, and scheduled workflows.

Why background jobs help

Some work is too slow or unreliable for a synchronous API request.

Examples:

  • Send a welcome email.
  • Generate a large CSV export.
  • Resize uploaded images.
  • Sync data with a third-party service.
  • Recalculate analytics.
  • Process webhook events.

Moving this work to a background job keeps the API responsive and gives the system a place to retry failures.

Basic architecture

A common architecture looks like this:

API request -> database transaction -> enqueue job -> return response
                                      -> worker processes job

The job payload should be small and explicit:

{
  "type": "send_welcome_email",
  "userId": "user_123",
  "jobId": "job_456"
}

The worker can load the current user data by ID when it runs.

Queues and workers

Most background job systems use a queue.

PartRole
ProducerEnqueues a job.
QueueStores jobs until workers process them.
WorkerPulls jobs and performs work.
Retry policyDecides what happens after failure.

Read Message Queues Explained for the queue fundamentals.

Scheduled jobs

Some jobs run on a schedule:

  • Cleanup expired sessions.
  • Send weekly reports.
  • Rebuild search indexes.
  • Sync subscription status.
  • Archive old records.

Cron-like schedules are common, but be careful when the app runs on multiple instances. If every instance runs the same scheduled job, work may duplicate.

Use platform scheduling, leader election, a queue, or a distributed lock carefully when only one copy should run. Read Distributed Locks Explained before reaching for a lock.

Idempotency

Workers should be safe to run more than once.

A job can be retried after a timeout. A worker can crash after completing the side effect but before acknowledging the job. A queue can redeliver a message.

Use stable job IDs, unique constraints, processed-event tables, or idempotency keys to prevent duplicate side effects.

For example, if a job sends a billing receipt, store that the receipt was sent for a specific invoice before sending another copy.

Read Idempotency in APIs Explained for the general pattern.

Status tracking

Long-running jobs often need status:

{
  "jobId": "job_456",
  "status": "processing",
  "progress": 42
}

Common statuses:

  • queued
  • processing
  • succeeded
  • failed
  • cancelled

APIs that create long-running jobs often return 202 Accepted with a status URL. Read REST API Status Codes Explained for 202 and related response patterns.

Monitoring

Background job systems need monitoring because failures are easy to hide.

Track:

  • Queue depth.
  • Oldest job age.
  • Job success rate.
  • Retry rate.
  • Dead-letter count.
  • Worker runtime.
  • Jobs by type.

A job queue can look healthy at the API layer while silently building a backlog. Queue age is often more important than queue length.

Launch checklist for background jobs

Before shipping a background job workflow, review the full lifecycle rather than only the happy path.

Useful questions:

  • Who creates the job and inside which transaction?
  • What unique key prevents duplicate jobs for the same business action?
  • How many workers can run at once without overloading dependencies?
  • What happens when the worker crashes after the side effect succeeds?
  • Where do failed jobs go after retries are exhausted?
  • Who receives alerts when the queue is old, not just large?

For example, an export job might be safe to retry because it writes output to the same object key each time. A payment capture job needs stronger protection: the job should carry a stable payment intent ID, the payment provider should receive an idempotency key, and the database should record the final provider response. That design lets the worker retry after timeouts without charging twice.

Failure handling

A mature job system separates temporary failure from permanent failure. A temporary failure might be a 503 from an email provider. A permanent failure might be a deleted user account or invalid configuration. Treating both the same wastes retry capacity and hides real data problems.

Store enough failure context to debug safely: job type, attempt count, last error category, timestamps, and a request or correlation ID. Avoid storing secrets or full payloads in job logs. When a job reaches a dead-letter queue, operators should be able to inspect it, decide whether it is safe to replay, and record the repair action.

Common mistakes

The first mistake is putting huge payloads in jobs. Large payloads are harder to version, retry, and inspect. Store IDs and fetch current data when possible.

The second mistake is not making jobs idempotent. Retry behavior will eventually create duplicates.

The third mistake is not recording failure reasons. Operators need enough detail to repair failed jobs.

The fourth mistake is running scheduled jobs on every app instance without coordination.

Read Message Queues Explained for delivery and retry behavior, Retry Patterns Explained for backoff and jitter, and Idempotency in APIs Explained for duplicate-safe processing. Use the Unix Timestamp Converter when debugging scheduled job times and retry delays.