Skip to content

Background Job Processing β€” Celery, Sidekiq, and Bull Explained

DodaTech Updated 2026-06-22 9 min read

In this tutorial, you'll learn about Background Job Processing. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Background job processing moves time-consuming tasks out of the request-response cycle into separate worker processes, allowing web applications to respond quickly while heavy work completes asynchronously in the background.

What You'll Learn

By the end of this tutorial, you will implement background job processing with Celery (Python), Sidekiq (Ruby), and Bull (Node.js), configure queues and workers, handle retries and failures, and design production-grade job pipelines.

Why It Matters

Synchronous task execution blocks user requests, causing timeouts and poorη”¨ζˆ·δ½“ιͺŒ. Background Jobs free the request cycle, improve scalability, and enable features like email delivery, video transcoding, and report generation without slowing down the UI. Doda Browser uses Background Jobs to scan uploaded files for malware without blocking the upload response.

Real-World Use

A video processing platform accepts uploads and immediately returns a "processing" status. A Celery worker transcodes the video to multiple resolutions, generates thumbnails, and updates the database. The user polls for status while the work happens in the background.

Architecture Overview

graph LR
    subgraph "Application"
        A[Web Server] --> B[Message Broker]
    end
    subgraph "Worker Pool"
        B --> C[Worker 1]
        B --> D[Worker 2]
        B --> E[Worker N]
    end
    subgraph "Result Backend"
        C --> F[Redis / DB]
        D --> F
        E --> F
    end
    F --> G[Status Dashboard]
    style B fill:#f90,color:#fff

A Message Broker (Redis, RabbitMQ) holds tasks. Workers pull tasks from the broker, execute them, and optionally store results in a result backend. The application enqueues tasks without waiting for completion.

Table: Framework Comparison

Feature Celery (Python) Sidekiq (Ruby) Bull (Node.js)
Broker Redis, RabbitMQ, SQS Redis Redis
Task Scheduling Yes (Celery Beat) Yes (sidekiq-Cron) Yes (repeatable)
Retry Mechanism Exponential backoff Configurable retries Backoff strategies
Prioritization Multiple queues Multiple queues Priority levels
Result Backend Redis, DB, S3 Redis Redis
Monitoring Flower Sidekiq Web UI Arena / Bull Board

Python: Celery Setup

# celery_app.py
# Celery application configuration
from celery import Celery

app = Celery(
    'tasks',
    broker='redis://localhost:6379/0',
    backend='redis://localhost:6379/1',
    include=['tasks']
)

app.conf.update(
    task_serializer='json',
    accept_content=['json'],
    result_serializer='json',
    timezone='UTC',
    enable_utc=True,
    task_track_started=True,
    task_acks_late=True,
    worker_prefetch_multiplier=1,
    task_soft_time_limit=300,
    task_time_limit=600,
)
# tasks.py
# Background task definitions for Celery
from celery_app import app
import time

@app.task(bind=True, max_retries=3, default_retry_delay=10)
def send_email(self, recipient, subject, body):
    """Send an email asynchronously with retry support."""
    try:
        print(f"Preparing email to {recipient}: {subject}")
        # Simulate SMTP call
        time.sleep(2)
        print(f"Email sent to {recipient}")
        return {"recipient": recipient, "status": "sent"}
    except ConnectionError as exc:
        raise self.retry(exc=exc, countdown=60)

@app.task(bind=True, rate_limit='10/m')
def process_image(self, image_path, output_sizes=None):
    """Process an image with rate limiting."""
    if output_sizes is None:
        output_sizes = [(800, 600), (400, 300), (150, 150)]
    results = []
    for width, height in output_sizes:
        # Simulate image processing
        time.sleep(1)
        result = {
            "input": image_path,
            "output": f"{image_path}_thumb_{width}x{height}.jpg",
            "size": f"{width}x{height}",
        }
        results.append(result)
        self.update_state(state='PROGRESS', meta={
            'current': len(results),
            'total': len(output_sizes),
        })
    print(f"Image processed: {image_path}")
    return {"results": results, "status": "completed"}

@app.task
def generate_report(user_id, report_type):
    """Generate a daily or weekly report."""
    print(f"Generating {report_type} report for user {user_id}")
    time.sleep(5)
    report_url = f"/reports/{user_id}/{report_type}/2026-06-22.pdf"
    print(f"Report generated: {report_url}")
    return {"user_id": user_id, "report_url": report_url}

Expected output:

Preparing email to alice@example.com: Welcome!
Email sent to alice@example.com

Node.js: Bull Queue

// bull-queue.js
// Background job processing with Bull and Redis
const Queue = require('bull');

const emailQueue = new Queue('email', {
  redis: { host: 'localhost', port: 6379 },
  defaultJobOptions: {
    attempts: 5,
    backoff: { type: 'exponential', delay: 2000 },
    removeOnComplete: 100,
    removeOnFail: 50,
  },
});

emailQueue.process(async (job) => {
  const { to, subject, body } = job.data;
  console.log(`[Job ${job.id}] Processing email to ${to}`);

  // Progress updates
  await job.progress(10);
  // Simulate send
  await new Promise((resolve) => setTimeout(resolve, 1500));

  await job.progress(50);
  await new Promise((resolve) => setTimeout(resolve, 1000));

  await job.progress(100);
  console.log(`[Job ${job.id}] Email sent to ${to}`);
  return { sent: true, to, timestamp: Date.now() };
});

emailQueue.on('failed', (job, err) => {
  console.error(`[Job ${job.id}] Failed attempt ${job.attemptsMade}: ${err.message}`);
});

emailQueue.on('completed', (job, result) => {
  console.log(`[Job ${job.id}] Completed: ${JSON.stringify(result)}`);
});

// Enqueue jobs
async function addEmailJobs() {
  await emailQueue.add(
    { to: 'alice@example.com', subject: 'Welcome!', body: 'Thank you for joining' },
    { priority: 1 }
  );
  await emailQueue.add(
    { to: 'bob@example.com', subject: 'Your invoice', body: 'Invoice attached' },
    { delay: 3600000 }
  );
  console.log('Jobs added to queue');
}

addEmailJobs();

Expected output:

[Job 1] Processing email to alice@example.com
[Job 1] Email sent to alice@example.com
[Job 1] Completed: {"sent":true,"to":"alice@example.com","timestamp":1723456789012}

Ruby: Sidekiq Worker

# sidekiq_worker.rb
# Sidekiq background job worker
require 'sidekiq'

class ReportGenerationWorker
  include Sidekiq::Worker
  sidekiq_options queue: 'reports', retry: 3, backtrace: true

  def perform(user_id, report_type)
    logger.info "Generating #{report_type} report for user #{user_id}"

    # Simulate report generation
    sleep 4

    report_data = {
      user_id: user_id,
      type: report_type,
      generated_at: Time.now.iso8601,
      url: "/reports/#{user_id}/#{report_type}/latest"
    }

    # Store result
    Sidekiq.redis do |conn|
      conn.set("report:#{user_id}:#{report_type}", report_data.to_json)
    end

    logger.info "Report generated for user #{user_id}"
    report_data
  rescue StandardError => e
    logger.error "Report generation failed: #{e.message}"
    raise
  end
end

# Enqueue from controller
ReportGenerationWorker.perform_async(42, 'weekly')
# Schedule for later
ReportGenerationWorker.perform_in(86400, 43, 'daily')

Job Chaining Patterns

# job_chaining.py
# Chain Celery tasks using callbacks
from celery import chain, group, chord
from tasks import send_email, process_image, generate_report

# Chain: run tasks sequentially
pipeline = chain(
    send_email.s('alice@example.com', 'Welcome!', 'Body'),
    generate_report.s(42, 'summary')
)
pipeline()

# Group: run tasks in parallel
parallel_tasks = group(
    process_image.s('/uploads/photo1.jpg'),
    process_image.s('/uploads/photo2.jpg'),
    process_image.s('/uploads/photo3.jpg'),
)
result = parallel_tasks()

# Chord: group + callback when all complete
callback = generate_report.s(42, 'summary')
workflow = chord(
    [process_image.s(f'/uploads/photo{i}.jpg') for i in range(5)],
    callback
)
workflow()

Expected output:

Preparing email to alice@example.com: Welcome!
Email sent to alice@example.com
Generating summary report for user 42
Report generated: /reports/42/summary/2026-06-22.pdf

Common Errors

1. Not Setting Task Timeouts

A task that hangs due to an external service timeout blocks a worker slot indefinitely. Always set task_time_limit (hard) and task_soft_time_limit (interrupt) for every task.

2. Ignoring Idempotency

If a task runs twice due to a retry, it should produce the same outcome. Sending a welcome email twice may be acceptable, but charging a payment twice is catastrophic. Design tasks to be idempotent by checking a deduplication key before processing.

3. Using the Same Redis for Everything

Sharing a single Redis instance between queues, caching, sessions, and Rate Limiting causes resource contention under load. Use separate Redis databases or dedicated instances for the job Queue.

4. Running Too Few Workers

If the Queue grows faster than workers can Process, latency increases linearly. Monitor Queue depth and scale workers horizontally. Use autoscaling based on Queue length metrics.

5. Not Handling Worker Graceful Shutdown

Killing workers while they Process tasks causes mid-flight tasks to be lost. Configure worker shutdown timeout (Celery: worker_shutdown_timeout, Sidekiq: -t) to allow in-progress tasks to finish.

6. Overloading the Result Backend

Storing large result payloads in Redis consumes memory. Truncate or avoid storing large results. Use a database or object storage for large outputs, and store only a reference in the result backend.

Practice Questions

1. What is the difference between a task Queue and a message Queue?

A task Queue (Celery, Sidekiq, Bull) manages job execution with worker processes, retries, scheduling, and result storage. A message Queue (RabbitMQ, Kafka) focuses on reliable message delivery between services without built-in job execution semantics.

2. How does Celery Beat work?

Celery Beat is a scheduler that enqueues periodic tasks at configured intervals. It stores the schedule in the broker or a database and launches tasks at the specified times, similar to Cron but integrated with the Celery worker pool.

3. What causes a task to be retried?

Tasks are retried when they raise a retryable exception (ConnectionError, TimeoutError). Celery catches the exception and re-enqueues the task. Non-retryable exceptions (ValueError, TypeError) should not trigger retries because they indicate programming errors.

4. How do you monitor Background Jobs?

Celery provides Flower (web UI), Sidekiq has a built-in web dashboard, and Bull has Bull Board or Arena. These dashboards show Queue depth, active workers, failed jobs, retry rates, and processing times.

5. Challenge: Build a multi-stage order processing pipeline with Celery that: (1) validates inventory against the database (2) charges the payment gateway with retry and exponential backoff (3) sends confirmation email (4) updates shipping status. Use a chain for sequential steps, separate queues for payment (high priority) and email (low priority), and implement a dead-letter Queue for orders that fail after all retries.

Mini Project: Job Dashboard with Flower

# flower_config.py
# Flower configuration for Celery monitoring
from flower.utils import template

# Custom flower template configuration
flower_config = {
    'port': 5555,
    'broker_API': 'Redis://localhost:6379/0',
    'address': '0.0.0.0',
    'db': 0,
    'max_tasks': 10000,
    'persistent': True,
    'State_save_interval': 10000,
    'auto_refresh': True,
    'natural_time': True,
    'tasks_routes': True,
    'enable_events': True,
}

# Custom metrics dashboard
# Run: Celery -A Celery_app flower --conf=flower_config.py

# API endpoints for external monitoring
# /dashboard/ -> Queue overview
# /tasks -> Active, pending, failed counts
# /workers -> Worker status and load

Run the dashboard:

Celery -A Celery_app flower --port=5555 --auto_refresh=True

Expected output:

[I 2026-06-22 10:00:00] Visit me at http://localhost:5555
[I 2026-06-22 10:00:01] Connected to redis://localhost:6379/0
[I 2026-06-22 10:00:01] Dashboard shows: queues, workers, tasks

FAQ

What broker should I use with Celery? Redis is the simplest choice for most applications. RabbitMQ offers more advanced routing, persistence guarantees, and supports AMQP. AWS SQS is suitable for Serverless deployments with Celery.

How many workers should I run? Start with 2 workers per CPU core. If tasks are I/O-bound (HTTP requests, database queries), run 2-4x more workers than cores. If CPU-bound, match workers to cores. Monitor Queue depth and adjust.

Can I prioritize certain jobs? Yes. Use separate queues for different priority levels. Configure workers to Process from high-priority queues first (Celery: task_create_missing_queues, Bull: priority option). Process payments in the critical Queue and emails in the low Queue.

What happens to unacknowledged tasks? If a worker crashes before acknowledging a task, the broker re-delivers the task to another worker (Celery: task_acks_late=True). This guarantees at-least-once delivery but requires idempotent tasks.

How do I schedule recurring jobs? Celery Beat handles periodic scheduling. Bull supports repeatable jobs with Cron syntax. Sidekiq uses sidekiq-Cron for recurring tasks. Store the schedule in a configuration file or database.

Related Concepts

Celery Task Queue
Message Queue Patterns
Rate Limiting

What's Next

You now understand background job processing with Celery, Sidekiq, and Bull. Next, learn about message Queue patterns for advanced broker configurations, then explore Celery for production deployment tuning.

  • Practice daily β€” Convert one synchronous endpoint in your app to use a background job
  • Build a project β€” Build a video processing pipeline with Celery: upload, transcode, thumbnail, and notify
  • Explore related topics β€” Check out job deduplication, batch processing, and dead-letter queues

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro