Skip to content

Introduction to Background Jobs

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Introduction to Background Jobs. We cover key concepts, practical examples, and best practices to help you master this topic.

Background jobs move time-consuming tasks out of the request-response cycle into separate worker processes for better application performance and user experience.

What You Learn

You will learn what background jobs are, the difference between synchronous and asynchronous processing, common use cases, and how background job systems work.

Why It Matters

Web applications must respond to HTTP requests in milliseconds. Operations like sending emails, processing images, or generating reports take seconds or minutes. Without background jobs, these operations block the user's request, causing timeouts and poor user experience.

Real-World Use

Doda Browser uses background jobs for every non-critical operation. When you upload a file, the response is immediate. A background job scans the file, generates thumbnails, and updates the status. You never wait for these operations to finish.

What Are Background Jobs?

flowchart LR
    U[User] -->|Request| W[Web Server]
    W -->|Immediate Response| U
    W -->|Enqueue Job| Q[Job Queue]
    Q -->|Process| WK[Worker 1]
    Q -->|Process| WK2[Worker 2]
    Q -->|Process| WK3[Worker 3]
    WK -->|Update| DB[Database]
    style Q fill:#f90,color:#fff

Background jobs are tasks that execute outside the main application flow. The web server queues a job and returns a response immediately. A worker process picks up the job and executes it asynchronously.

Key Concepts

Concept Description Example
Job Queue Stores pending jobs Redis list, RabbitMQ queue
Worker Process that executes jobs Python process, Node.js worker
Producer Code that creates jobs Web request handler
Serializer Converts job data JSON, msgpack
Result Store Stores job outcomes Redis, database

Synchronous vs Asynchronous

import time

# Synchronous - blocks until complete
def send_email_sync(to, subject):
    print(f"Sending email to {to}...")
    time.sleep(2)  # Simulate network call
    print(f"Email sent to {to}")
    return "sent"

# The caller waits 2 seconds
result = send_email_sync("user@example.com", "Welcome")
print(f"Result: {result}")  # User waited 2 seconds for this

Expected output:

Sending email to user@example.com...
Email sent to user@example.com
Result: sent
# Asynchronous with background job
import redis
import json
import time

r = redis.Redis()
job = {'to': 'user@example.com', 'subject': 'Welcome'}
r.lpush('email_queue', json.dumps(job))
print("Email queued, response immediate")

Expected output:

Email queued, response immediate

Common Use Cases

Use Case Why Background Processing Time
Email delivery SMTP connections are slow 1-5 seconds
Image processing CPU-intensive 0.5-10 seconds
Report generation Data aggregation 5-60 seconds
Webhook delivery Network calls 1-30 seconds
Data import/export File I/O 10-600 seconds
Video transcoding Very CPU-intensive 30-3600 seconds
Cache warming Database queries 5-60 seconds

Job Processing Flow

import redis
import json
import time
import threading

r = redis.Redis()

# Producer - enqueues jobs
def produce_jobs():
    for i in range(5):
        job = {'task_id': i, 'data': f'item_{i}'}
        r.lpush('work_queue', json.dumps(job))
        print(f"Enqueued job {i}")
        time.sleep(0.1)

# Worker - processes jobs
def worker(name):
    while True:
        job_data = r.brpop('work_queue', timeout=5)
        if job_data:
            _, data = job_data
            job = json.loads(data)
            print(f"[{name}] Processing job {job['task_id']}")
            time.sleep(0.5)
            print(f"[{name}] Job {job['task_id']} done")
        else:
            break

# Run
producer_thread = threading.Thread(target=produce_jobs, daemon=True)
worker1 = threading.Thread(target=worker, args=('W1',), daemon=True)
worker2 = threading.Thread(target=worker, args=('W2',), daemon=True)

producer_thread.start()
worker1.start()
worker2.start()
time.sleep(4)

Expected output:

Enqueued job 0
Enqueued job 1
Enqueued job 2
Enqueued job 3
Enqueued job 4
[W1] Processing job 0
[W2] Processing job 1
[W1] Job 0 done
[W2] Job 1 done
[W1] Processing job 2
[W2] Processing job 3
[W1] Job 2 done
[W2] Job 3 done
[W1] Processing job 4
[W1] Job 4 done

Background Job System Components

# Simple background job system
import redis
import json
from functools import wraps

r = redis.Redis()

class BackgroundJob:
    def __init__(self, queue='default'):
        self.queue = queue

    def enqueue(self, func, *args, **kwargs):
        job = {
            'func': f"{func.__module__}.{func.__name__}",
            'args': args,
            'kwargs': kwargs,
        }
        r.lpush(self.queue, json.dumps(job))
        return job

def background(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        job = BackgroundJob()
        return job.enqueue(func, *args, **kwargs)
    return wrapper

@background
def process_file(path):
    print(f"Processing {path}")
    return "done"

@background
def send_email(to, subject):
    print(f"Sending email to {to}: {subject}")
    return "sent"

# These execute in background
process_file("/data/file.txt")
send_email("user@example.com", "Welcome")

print("Main thread continues immediately")

Expected output:

Main thread continues immediately

Common Mistakes

1. Using Background Jobs for Everything

Not everything needs a background job. If an operation takes under 100ms, execute it synchronously. Background jobs add complexity and should only be used when the delay matters.

2. Not Handling Worker Failures

If a worker crashes, in-progress jobs are lost without proper acknowledgment mechanisms. Use job acknowledgements (acks) to ensure reliable processing.

3. Ignoring Job Queue Backpressure

A producer that enqueues jobs faster than workers process them causes queue growth. Monitor queue depth and set up backpressure mechanisms.

4. Passing Non-Serializable Data

Jobs must be serializable (JSON, msgpack). Do not pass database connections, file handles, or complex objects as job arguments.

5. Forgetting Job Deduplication

The same job may be enqueued multiple times (user double-clicks submit). Implement idempotency or deduplication at the job level.

Practice Questions

1. What is a background job?

A task executed outside the main request-response cycle by a separate worker process. The web server queues the job and returns immediately.

2. What is the difference between sync and async processing?

Synchronous: the caller waits for completion. Asynchronous: the caller queues the work and continues. Background jobs use asynchronous processing.

3. What components make up a background job system?

Producer (enqueues jobs), Queue (stores pending jobs), Worker (processes jobs), and optionally a Result Store (stores outcomes).

4. What types of data can be passed to a background job?

Serializable data: strings, numbers, lists, dictionaries. Pass IDs for database records rather than the records themselves.

Challenge

Design a background job system for a video sharing platform. Users upload videos, and the system must: validate format, transcode to multiple resolutions, generate thumbnails, analyze content, store metadata, and notify the user. Map each step with time estimates and choose appropriate job processing strategies.

FAQ

Are background jobs the same as cron jobs?

No. Background jobs are queued and processed immediately by workers. Cron jobs run on a fixed schedule. They serve different purposes.

What is the best queue storage for background jobs?

Redis is the most common for simplicity and speed. RabbitMQ is better for complex routing. Amazon SQS is good for fully managed solutions.

How many workers should I run?

Start with 2-4 workers per CPU core. Monitor queue depth and worker utilization. Add workers if the queue grows consistently.

Can background jobs run on a different server?

Yes. Workers can run on any machine that can reach the queue storage. This is how background job systems scale horizontally.

What happens if the queue storage crashes?

Pending jobs are lost if the queue is not persistent. Use Redis with persistence, RabbitMQ with mirrored queues, or SQS for durability.

Mini Project: Simple Background Job System

# simple_bg.py
import redis
import json
import time
import threading
from functools import wraps

r = redis.Redis()

class SimpleWorker:
    def __init__(self, queue='default'):
        self.queue = queue
        self.handlers = {}
        self.running = False

    def register(self, name):
        def decorator(func):
            self.handlers[name] = func
            return func
        return decorator

    def enqueue(self, task_name, *args, **kwargs):
        job = {
            'task': task_name,
            'args': args,
            'kwargs': kwargs,
            'enqueued_at': time.time(),
        }
        r.lpush(self.queue, json.dumps(job))
        print(f"Enqueued: {task_name}")

    def process_job(self, job_data):
        try:
            job = json.loads(job_data)
            task_name = job['task']
            handler = self.handlers.get(task_name)
            if handler:
                print(f"Processing: {task_name}")
                result = handler(*job['args'], **job['kwargs'])
                print(f"Completed: {task_name} -> {result}")
            else:
                print(f"No handler for: {task_name}")
        except Exception as e:
            print(f"Failed: {e}")

    def start(self):
        self.running = True
        while self.running:
            job_data = r.brpop(self.queue, timeout=1)
            if job_data:
                _, data = job_data
                self.process_job(data)

    def stop(self):
        self.running = False

# Usage
worker = SimpleWorker()

@worker.register('send_email')
def send_email(to, subject):
    time.sleep(0.5)
    return f"Email sent to {to}"

@worker.register('process_image')
def process_image(path):
    time.sleep(1)
    return f"Image processed: {path}"

# Enqueue jobs
worker.enqueue('send_email', 'user@example.com', 'Welcome')
worker.enqueue('process_image', '/uploads/photo.jpg')

# Start worker in background thread
t = threading.Thread(target=worker.start, daemon=True)
t.start()

time.sleep(3)
print("Done")

Expected output:

Enqueued: send_email
Enqueued: process_image
Processing: send_email
Completed: send_email -> Email sent to user@example.com
Processing: process_image
Completed: process_image -> Image processed: /uploads/photo.jpg
Done

What's Next

Now that you understand background jobs, learn about synchronous vs asynchronous processing in detail, then explore common use cases for background jobs.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro