backend / message queues / celery / 01_what_is_celery.md

What is Celery?

4 interview angles 8 min read source

What is Celery?

Definition

Celery is a distributed task queue system for Python that enables asynchronous execution of tasks across multiple worker processes or machines. It’s designed to handle large volumes of messages while providing the tools needed to maintain such a system.

Key Concepts

Core Components

  1. Celery Worker: A process that executes tasks asynchronously. Workers can run on the same machine or be distributed across multiple machines.

  2. Celery Beat: A scheduler that runs periodic tasks (like cron jobs) at specified intervals.

  3. Message Broker: The messaging system that stores and routes messages between the application and workers. Common brokers include:

    • RabbitMQ (recommended for production)
    • Redis (good for development and small-scale production)
    • Amazon SQS
    • Apache Kafka
  4. Result Backend: Stores the results of tasks. Can be:

    • Redis
    • RabbitMQ
    • Database (PostgreSQL, MySQL, etc.)
    • Memcached
    • RPC (for real-time results)
  5. Task: A function decorated with @celery.task that can be executed asynchronously.

Architecture Flow

Application → Broker → Worker → Result Backend
     ↓           ↓        ↓           ↓
  Sends      Stores   Executes    Stores
  Task      Message    Task       Result

When to Use Celery

  • Long-running tasks: Image processing, video encoding, data analysis
  • Periodic tasks: Scheduled reports, cleanup jobs, data synchronization
  • Background jobs: Sending emails, generating PDFs, API calls to external services
  • Distributed computing: Tasks that need to run across multiple machines
  • Task prioritization: Different queues for different priority levels
  • Rate limiting: Control how many tasks run simultaneously

Basic Example

from celery import Celery

# Create Celery instance
app = Celery('tasks', broker='redis://localhost:6379/0')

# Define a task
@app.task
def add(x, y):
    return x + y

# Call the task asynchronously
result = add.delay(4, 4)
print(result.get())  # Wait for result: 8

Common Interview Questions and Answers

1. What is Celery and when should you use it?

Celery is a distributed task queue system for Python that allows you to run tasks asynchronously in the background. Use it when:

  • You have time-consuming operations that shouldn’t block the main application
  • You need to process tasks in parallel across multiple workers
  • You want to schedule periodic tasks
  • You need to handle high volumes of background jobs

2. Explain the difference between Celery and traditional message queues

  • Traditional message queues (like RabbitMQ) are just message brokers - they store and route messages
  • Celery is a complete task queue framework that includes:
    • Task definition and serialization
    • Worker management
    • Result storage
    • Task scheduling (Beat)
    • Task routing and prioritization
    • Built-in retry mechanisms
    • Task monitoring

Celery uses message queues as brokers but adds a higher-level abstraction for task management.

3. How does Celery handle task distribution?

  • Tasks are sent to a broker (message queue)
  • Workers connect to the broker and consume tasks
  • Celery uses a prefetch mechanism where workers reserve a certain number of tasks
  • Tasks are distributed using round-robin by default
  • You can use task routing to send specific tasks to specific queues/workers
  • Task priorities can be used to process high-priority tasks first

4. What are the different Celery brokers and their trade-offs?

RabbitMQ:

  • Most reliable and feature-rich
  • Supports complex routing
  • Good for production
  • Requires separate service
  • More complex setup

Redis:

  • Simple setup
  • Can be used as both broker and result backend
  • Good for development
  • Can lose messages if not configured properly
  • Less reliable than RabbitMQ for critical tasks

Amazon SQS:

  • Fully managed
  • Highly scalable
  • Good for cloud deployments
  • Vendor lock-in
  • Additional costs

5. How to implement periodic tasks with Celery Beat?

from celery import Celery
from celery.schedules import crontab

app = Celery('tasks', broker='redis://localhost:6379/0')

# Define periodic task schedule
app.conf.beat_schedule = {
    'send-daily-report': {
        'task': 'tasks.send_report',
        'schedule': crontab(hour=9, minute=0),  # Every day at 9 AM
    },
    'cleanup-old-data': {
        'task': 'tasks.cleanup',
        'schedule': 3600.0,  # Every hour
    },
}

@app.task
def send_report():
    # Send report logic
    pass

Run Beat scheduler: celery -A tasks beat

6. Explain Celery task routing and queues

Task routing allows you to send specific tasks to specific queues, which can be processed by dedicated workers.

# Configure task routes
app.conf.task_routes = {
    'tasks.send_email': {'queue': 'email'},
    'tasks.process_image': {'queue': 'image_processing'},
    'tasks.generate_report': {'queue': 'reports'},
}

# Start workers for specific queues
# celery -A tasks worker -Q email,image_processing

Benefits:

  • Isolate different types of tasks
  • Scale workers independently
  • Set different priorities per queue
  • Use different worker configurations per queue

7. How does Celery handle task retries and error handling?

Celery provides built-in retry mechanisms:

@app.task(bind=True, max_retries=3)
def my_task(self, x, y):
    try:
        # Task logic
        result = x / y
        return result
    except ZeroDivisionError as exc:
        # Retry with exponential backoff
        raise self.retry(exc=exc, countdown=60)

Key parameters:

  • max_retries: Maximum number of retry attempts
  • countdown: Seconds to wait before retry
  • exponential_backoff: Automatically increase wait time
  • retry_backoff: Enable exponential backoff
  • retry_backoff_max: Maximum wait time

8. What are Celery signals and how to use them?

Celery signals allow you to hook into task lifecycle events:

from celery.signals import task_prerun, task_postrun, task_failure

@task_prerun.connect
def task_prerun_handler(sender=None, task_id=None, task=None, args=None, kwargs=None, **kwds):
    print(f"Task {task_id} is about to run")

@task_postrun.connect
def task_postrun_handler(sender=None, task_id=None, task=None, args=None, kwargs=None, retval=None, state=None, **kwds):
    print(f"Task {task_id} finished with state {state}")

@task_failure.connect
def task_failure_handler(sender=None, task_id=None, exception=None, traceback=None, einfo=None, **kwds):
    print(f"Task {task_id} failed: {exception}")

Common signals:

  • task_prerun: Before task execution
  • task_postrun: After task execution
  • task_failure: On task failure
  • task_success: On task success
  • task_retry: On task retry

9. How to implement task monitoring and logging?

import logging
from celery import Celery
from celery.utils.log import get_task_logger

app = Celery('tasks', broker='redis://localhost:6379/0')
logger = get_task_logger(__name__)

@app.task
def monitored_task():
    logger.info("Task started")
    try:
        # Task logic
        result = perform_operation()
        logger.info(f"Task completed: {result}")
        return result
    except Exception as e:
        logger.error(f"Task failed: {str(e)}", exc_info=True)
        raise

For monitoring:

  • Flower: Web-based tool for monitoring Celery clusters
  • Celery events: Real-time monitoring API
  • Logging: Configure Python logging for Celery

10. Explain Celery task serialization and deserialization

Celery needs to serialize tasks and arguments to send them through the broker.

Supported serializers:

  • json: Default, human-readable, limited types
  • pickle: Python-specific, supports all Python objects (security risk)
  • yaml: Human-readable, slower
  • msgpack: Binary, fast, compact
# Configure serialization
app.conf.task_serializer = 'json'
app.conf.accept_content = ['json']
app.conf.result_serializer = 'json'

Best Practice: Use json for security, only use pickle if absolutely necessary and with trusted sources.

11. How to handle task priorities in Celery?

# Define task with priority
@app.task
def high_priority_task():
    pass

# Send task with priority
high_priority_task.apply_async(args=[], priority=9)  # 0-9, higher is more priority

# Configure worker to respect priorities
# celery -A tasks worker -Q celery --prefetch-multiplier=1

Note: Priorities work best with RabbitMQ. Redis doesn’t natively support priorities.

12. What are Celery worker pools and their types?

Worker pools determine how tasks are executed:

  1. prefork (default):

    • Uses multiprocessing
    • Good for CPU-bound tasks
    • Isolated processes
    • More memory usage
  2. solo:

    • Single-threaded
    • Good for debugging
    • No parallelism
  3. gevent:

    • Uses greenlets (cooperative multitasking)
    • Good for I/O-bound tasks
    • Many concurrent tasks
    • Less memory than prefork
  4. eventlet:

    • Similar to gevent
    • Uses eventlet library
# Start worker with specific pool
celery -A tasks worker --pool=gevent --concurrency=1000

13. How to implement task result caching?

from celery import Celery
from functools import lru_cache

app = Celery('tasks', broker='redis://localhost:6379/0')

# Option 1: Use result backend caching
app.conf.result_backend = 'redis://localhost:6379/0'
app.conf.result_expires = 3600  # Results expire in 1 hour

# Option 2: Custom caching in task
@app.task
def expensive_computation(n):
    # Check cache first
    cache_key = f"computation_{n}"
    cached_result = redis_client.get(cache_key)
    if cached_result:
        return cached_result

    # Perform computation
    result = compute(n)

    # Cache result
    redis_client.setex(cache_key, 3600, result)
    return result

14. Explain Celery task cancellation and revocation

# Revoke a task (prevent it from running)
from celery import Celery

app = Celery('tasks', broker='redis://localhost:6379/0')

# Revoke by task ID
app.control.revoke('task-id-123', terminate=True)

# Revoke in task
@app.task(bind=True)
def cancellable_task(self):
    if self.is_aborted():
        return "Task was cancelled"
    # Task logic

Revocation types:

  • Soft revoke: Task won’t start if not already running
  • Terminate: Kill running task (sends SIGTERM)

15. How to handle Celery in a microservices architecture?

  1. Shared Broker: All services use the same broker

    # Service A
    app = Celery('service_a', broker='rabbitmq://shared-broker')
    
    # Service B
    app = Celery('service_b', broker='rabbitmq://shared-broker')
  2. Separate Queues per Service: Use routing to isolate services

    app.conf.task_routes = {
        'service_a.*': {'queue': 'service_a'},
        'service_b.*': {'queue': 'service_b'},
    }
  3. Task Communication: Services can call tasks from other services

    # Service A calls Service B task
    from service_b.tasks import process_data
    result = process_data.delay(data)
  4. Best Practices:

    • Use separate result backends per service
    • Implement proper error handling
    • Use message versioning
    • Monitor cross-service task calls

Interview angle

  • “What is Celery and when do you use it?” - a distributed task queue for work that shouldn’t happen inside a request: sending mail, generating reports, calling slow third parties, scheduled jobs. It needs a broker (Redis or RabbitMQ) and optionally a result backend.
  • “Broker versus result backend?” - the broker transports task messages; the result backend stores return values and state. You can run without a result backend, and often should, since storing results you never read is pure overhead.
  • “What delivery guarantee do you get?” - at-least-once with acks_late, so a worker crash re-delivers the task. That makes idempotency mandatory. Default early ack gives at-most-once and silently loses work on a crash.
  • “How do you stop one slow task type starving everything?” - separate queues with dedicated workers, so a slow report job can’t occupy the workers handling fast user-facing tasks. Routing by queue is the main operational lever.