| name | celery |
| description | Distributed task queue system for Python enabling asynchronous execution of background jobs, scheduled tasks, and workflows across multiple workers with Django, Flask, and FastAPI integration. |
| user-invocable | false |
| disable-model-invocation | true |
| progressive_disclosure | {"entry_point":["summary","when_to_use","quick_start"],"sections":["core_concepts","broker_setup","task_basics","task_execution","task_routing","periodic_tasks","workflows","error_handling","monitoring","framework_integration","testing","production_patterns","performance","use_cases","alternatives","best_practices","troubleshooting"]} |
Celery: Distributed Task Queue
Summary
Celery is a distributed task queue system for Python that enables asynchronous execution of background jobs across multiple workers. It supports scheduling, retries, task workflows, and integrates seamlessly with Django, Flask, and FastAPI.
When to Use
- Background Processing: Offload long-running operations (email, file processing, reports)
- Scheduled Tasks: Cron-like periodic jobs (cleanup, backups, data sync)
- Distributed Computing: Process tasks across multiple workers/servers
- Async Workflows: Chain, group, and orchestrate complex task dependencies
- Real-time Processing: Handle webhooks, notifications, data pipelines
- Load Balancing: Distribute CPU-intensive work across workers
Don't Use When:
- Simple async I/O (use
asyncio instead)
- Real-time request/response (use async web frameworks)
- Sub-second latency required (use in-memory queues)
- Minimal infrastructure (use simpler alternatives like RQ or Huey)
Quick Start
Installation
pip install celery
pip install celery[redis]
pip install celery[amqp]
pip install celery[redis,msgpack,auth,cassandra,elasticsearch,s3,sqs]
Basic Setup
from celery import Celery
app = Celery(
'myapp',
broker='redis://localhost:6379/0',
backend='redis://localhost:6379/1'
)
app.conf.update(
task_serializer='json',
accept_content=['json'],
result_serializer='json',
timezone='UTC',
enable_utc=True,
)
@app.task
def add(x, y):
return x + y
@app.task
def send_email(to, subject, body):
import time
time.sleep(2)
print(f"Email sent to {to}: {subject}")
return {"status": "sent", "to": to}
Running Workers
celery -A celery_app worker --loglevel=info
celery -A celery_app worker --concurrency=4 --loglevel=info
celery -A celery_app worker -Q emails,reports --loglevel=info
Executing Tasks
result = add.delay(4, 6)
print(result.get(timeout=10))
result = send_email.apply_async(
args=['user@example.com', 'Hello', 'Welcome!'],
countdown=60
)
print(result.status)
Core Concepts
Architecture Components
Broker: Message queue that stores tasks
- Redis (recommended for most use cases)
- RabbitMQ (enterprise-grade, complex)
- Amazon SQS (serverless, AWS-native)
Workers: Processes that execute tasks
- Pull tasks from broker
- Execute task code
- Store results in backend
Result Backend: Storage for task results
- Redis (fast, in-memory)
- Database (PostgreSQL, MySQL)
- S3 (large results)
- Cassandra, Elasticsearch (specialized)
Beat Scheduler: Periodic task scheduler
- Cron-like scheduling
- Interval-based tasks
- Stores schedule in database or file
Task States
PENDING → STARTED → SUCCESS
→ RETRY → SUCCESS
→ FAILURE
- PENDING: Task waiting in queue
- STARTED: Worker picked up task
- SUCCESS: Task completed successfully
- FAILURE: Task raised exception
- RETRY: Task will retry after failure
- REVOKED: Task cancelled before execution
Broker Setup
Redis Configuration
broker_url = 'redis://localhost:6379/0'
result_backend = 'redis://localhost:6379/1'
broker_url = 'redis://:password@localhost:6379/0'
broker_url = 'sentinel://localhost:26379;sentinel://localhost:26380'
broker_transport_options = {
'master_name': 'mymaster',
'sentinel_kwargs': {'password': 'password'},
}
broker_pool_limit = 10
broker_connection_retry = True
broker_connection_retry_on_startup = True
broker_connection_max_retries = 10
RabbitMQ Configuration
broker_url = 'amqp://guest:guest@localhost:5672//'
broker_url = 'amqp://user:password@localhost:5672/myvhost'
broker_url = [
'amqp://user:password@host1:5672//',
'amqp://user:password@host2:5672//',
]
broker_heartbeat = 30
broker_pool_limit = 10
Amazon SQS Configuration
broker_url = 'sqs://'
broker_transport_options = {
'region': 'us-east-1',
'queue_name_prefix': 'myapp-',
'visibility_timeout': 3600,
'polling_interval': 1,
}
import boto3
broker_transport_options = {
'region': 'us-east-1',
'predefined_queues': {
'default': {
'url': 'https://sqs.us-east-1.amazonaws.com/123456789/myapp-default',
}
}
}
Task Basics
Task Definition
from celery import Task, shared_task
from celery_app import app
@app.task
def simple_task(x, y):
return x + y
@shared_task
def framework_task(data):
return process(data)
class CustomTask(Task):
def on_success(self, retval, task_id, args, kwargs):
print(f"Task {task_id} succeeded with {retval}")
def on_failure(self, exc, task_id, args, kwargs, einfo):
print(f"Task {task_id} failed: {exc}")
def on_retry(self, exc, task_id, args, kwargs, einfo):
print(f"Task {task_id} retrying: {exc}")
@app.task(base=CustomTask)
def monitored_task(x):
return x * 2
Task Options
@app.task(
name='custom.task.name',
bind=True,
ignore_result=True,
max_retries=3,
default_retry_delay=60,
rate_limit='100/h',
time_limit=300,
soft_time_limit=240,
serializer='json',
compression='gzip',
priority=5,
queue='high_priority',
routing_key='priority.high',
acks_late=True,
reject_on_worker_lost=True,
)
def advanced_task(self, data):
try:
return process(data)
except Exception as exc:
raise self.retry(exc=exc, countdown= ** .request.retries)
Task Context (bind=True)
@app.task(bind=True)
def context_aware_task(self, x, y):
print(f"Task ID: {self.request.id}")
print(f"Task Name: {self.name}")
print(f"Args: {self.request.args}")
print(f"Kwargs: {self.request.kwargs}")
print(f"Retries: {self.request.retries}")
print(f"Delivery Info: {self.request.delivery_info}")
try:
result = risky_operation(x, y)
except Exception as exc:
raise self.retry(exc=exc, countdown=60, max_retries=3)
return result
Task Execution
Delay vs Apply Async
result = add.delay(4, 6)
result = add.apply_async(
args=(4, 6),
kwargs={'extra': 'data'},
countdown=60,
eta=datetime(2025, 12, 1, 10, 0),
expires=3600,
queue='math',
routing_key='math.add',
exchange='tasks',
priority=9,
serializer='json',
compression='gzip',
retry=True,
retry_policy={
'max_retries': 3,
'interval_start': 0,
'interval_step': 0.2,
'interval_max': 0.2,
},
link=log_result.s(),
link_error=handle_error.s(),
)
result.ready():
(result.get())
(result.result)
Task Signatures
from celery import signature
sig = add.signature((2, 2), countdown=10)
sig = add.s(2, 2)
partial = add.s(2)
result = partial.apply_async(args=(4,))
immutable = add.si(2, 2)
new_sig = sig.clone(countdown=60)
result = sig.delay()
result = sig.apply_async()
result = sig()
Result Handling
result = add.delay(4, 6)
value = result.get(timeout=10)
if result.ready():
print(result.result)
print(result.status)
print(result.successful())
print(result.failed())
print(result.traceback)
print(result.info)
result.forget()
result.revoke(terminate=True)
add.AsyncResult(task_id).revoke()
Task Routing
Queue Configuration
from kombu import Queue, Exchange
app.conf.task_queues = (
Queue('default', Exchange('default'), routing_key='default'),
Queue('high_priority', Exchange('priority'), routing_key='priority.high'),
Queue('low_priority', Exchange('priority'), routing_key='priority.low'),
Queue('emails', Exchange('tasks'), routing_key='tasks.email'),
Queue('reports', Exchange('tasks'), routing_key='tasks.report'),
)
app.conf.task_default_queue = 'default'
app.conf.task_default_exchange = 'tasks'
app.conf.task_default_routing_key = 'default'
Task Routing Rules
app.conf.task_routes = {
'myapp.tasks.send_email': {'queue': 'emails'},
'myapp.tasks.generate_report': {'queue': 'reports', 'priority': 9},
'myapp.tasks.*': {'queue': 'default'},
}
def route_task(name, args, kwargs, options, task=None, **kw):
if 'email' in name:
return {'queue': 'emails', 'routing_key': 'email.send'}
elif 'report' in name:
return {'queue': 'reports', 'priority': 5}
return {'queue': 'default'}
app.conf.task_routes = (route_task,)
Worker Queue Binding
celery -A myapp worker -Q emails,reports --loglevel=info
celery -A myapp worker -Q high_priority -c 4 --loglevel=info
celery -A myapp worker -Q default -c 2 --loglevel=info
celery -A myapp worker -Q low_priority -c 1 --loglevel=info
Priority Queues
app.conf.task_queue_max_priority = 10
app.conf.task_default_priority = 5
high_priority_task.apply_async(args=(), priority=9)
low_priority_task.apply_async(args=(), priority=1)
app.conf.task_routes = {
'critical_task': {'queue': 'default', 'priority': 10},
'background_task': {'queue': 'default', 'priority': 1},
}
Periodic Tasks
Celery Beat Setup
from celery.schedules import crontab, solar
beat_schedule = {
'add-every-30-seconds': {
'task': 'myapp.tasks.add',
'schedule': 30.0,
'args': (16, 16)
},
'send-daily-report': {
'task': 'myapp.tasks.send_daily_report',
'schedule': crontab(hour=7, minute=30),
},
'weekly-cleanup': {
'task': 'myapp.tasks.cleanup',
'schedule': crontab(hour=0, minute=0, day_of_week=1),
},
'monthly-report': {
'task': 'myapp.tasks.monthly_report',
'schedule': crontab(hour=0, minute=0, day_of_month='1'),
'kwargs': {'month_offset': 1}
},
'wake-up-at-sunrise': {
'task': 'myapp.tasks.morning_routine',
'schedule': solar('sunrise', -37.81, 144.96),
},
}
app.conf.beat_schedule = beat_schedule
Crontab Patterns
from celery.schedules import crontab
crontab()
crontab(minute='*/15')
crontab(minute=30)
crontab(hour=0, minute=0)
crontab(hour=17, minute=0, day_of_week='1-5')
crontab(hour=12, minute=0, day_of_week='mon,wed,fri')
crontab(hour=0, minute=0, day_of_month='1')
crontab(hour=0, minute=0, day_of_month='28-31')
crontab(hour=0, minute=0, day_of_month='1', month_of_year='*/3')
Running Beat Scheduler
celery -A myapp beat --loglevel=info
celery -A myapp beat --scheduler django_celery_beat.schedulers:DatabaseScheduler
celery -A myapp worker --beat --loglevel=info
Dynamic Schedules (django-celery-beat)
pip install django-celery-beat
INSTALLED_APPS = [
'django_celery_beat',
]
python manage.py migrate django_celery_beat
celery -A myapp beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
from django_celery_beat.models import PeriodicTask, IntervalSchedule, CrontabSchedule
import json
schedule, created = IntervalSchedule.objects.get_or_create(
every=10,
period=IntervalSchedule.SECONDS,
)
PeriodicTask.objects.create(
interval=schedule,
name='Import feed every 10 seconds',
task='myapp.tasks.import_feed',
args=json.dumps(['https://example.com/feed']),
)
schedule, created = CrontabSchedule.objects.get_or_create(
minute='0',
hour='*/4',
day_of_week='*',
day_of_month='*',
month_of_year='*',
)
PeriodicTask.objects.create(
crontab=schedule,
name='Hourly cleanup',
task='myapp.tasks.cleanup',
)
Workflows (Canvas)
Chains
from celery import chain
result = chain(add.s(2, 2), add.s(4), add.s(8))()
result = (add.s(2, 2) | add.s(4) | add.s(8))()
workflow = (
fetch_data.s(url) |
process_data.s() |
save_results.s()
)
result = workflow.apply_async()
Groups
from celery import group
job = group([
add.s(2, 2),
add.s(4, 4),
add.s(8, 8),
])
result = job.apply_async()
results = result.get(timeout=10)
job = group([
process_item.s(item) for item in items
]) | summarize_results.s()
Chords
from celery import chord
job = chord([
fetch_url.s(url) for url in urls
])(combine_results.s())
workflow = chord([
process_file.s(file) for file in files
])(merge_results.s())
result = workflow.apply_async()
Map and Starmap
from celery import group
results = add.map([(2, 2), (4, 4), (8, 8)])
results = add.starmap([(2, 2), (4, 4), (8, 8)])
results = group([add.s(2, 2), add.s(4, 4), add.s(8, 8)])()
Complex Workflows
from celery import chain, group, chord
workflow = chain(
fetch_data.s(source),
group([
process_chunk.s(chunk_id) for chunk_id in range(10)
]),
aggregate.s(),
save_results.s()
)
workflow = chord([
chord([
subtask.s(item) for item in chunk
])(process_chunk.s())
for chunk in chunks
])(final_callback.s())
generate_report = chain(
fetch_user_data.s(user_id),
chord([
calculate_stats.s(),
fetch_transactions.s(),
fetch_activity.s(),
])(combine_sections.s()),
render_pdf.s(),
send_email.s(user_email)
)
Error Handling
Automatic Retries
@app.task(
autoretry_for=(RequestException, IOError),
retry_kwargs={'max_retries': 5},
retry_backoff=True,
retry_backoff_max=600,
retry_jitter=True,
)
def fetch_url(url):
response = requests.get(url)
response.raise_for_status()
return response.json()
Manual Retries
@app.task(bind=True, max_retries=3)
def process_data(self, data):
try:
result = external_api_call(data)
return result
except TemporaryError as exc:
raise self.retry(exc=exc, countdown=60)
except PermanentError as exc:
logger.error(f"Permanent error: {exc}")
raise
except Exception as exc:
raise self.retry(
exc=exc,
countdown=2 ** self.request.retries,
max_retries=3
)
Error Callbacks
@app.task
def on_error(request, exc, traceback):
"""Called when task fails"""
logger.error(f"Task {request.id} failed: {exc}")
send_alert(f"Task failure: {request.task}", str(exc))
@app.task
def risky_task(data):
return process(data)
risky_task.apply_async(
args=(data,),
link_error=on_error.s()
)
Task Failure Handling
from celery import Task
class CallbackTask(Task):
def on_failure(self, exc, task_id, args, kwargs, einfo):
"""Handle task failure"""
logger.error(f"Task {task_id} failed with {exc}")
send_notification('Task Failed', str(exc))
def on_success(self, retval, task_id, args, kwargs):
"""Handle task success"""
logger.info(f"Task {task_id} succeeded: {retval}")
def on_retry(self, exc, task_id, args, kwargs, einfo):
"""Handle task retry"""
logger.warning(f"Task {task_id} retrying: {exc}")
@app.task(base=CallbackTask)
def monitored_task(x):
if x < 0:
raise ValueError("Negative value")
return x * 2
Exception Handling Patterns
@app.task(bind=True)
def robust_task(self, data):
try:
return process(data)
except NetworkError as exc:
raise self.retry(exc=exc, countdown=60, max_retries=5)
except ValidationError as exc:
logger.error(f"Invalid data: {exc}")
return {'status': 'failed', 'error': str(exc)}
except DatabaseError as exc:
backoff = min(2 ** self.request.retries * 60, 3600)
raise self.retry(exc=exc, countdown=backoff, max_retries=10)
except Exception as exc:
if self.request.retries < 3:
raise self.retry(exc=exc, countdown=120)
else:
logger.critical()
send_alert(, (exc))
Monitoring and Management
Task Events
app.conf.worker_send_task_events = True
app.conf.task_send_sent_event = True
from celery import signals
@signals.task_prerun.connect
def task_prerun_handler(sender=None, task_id=None, task=None, args=None, kwargs=None, **extra):
print(f"Task {task.name}[{task_id}] starting")
@signals.task_postrun.connect
def task_postrun_handler(sender=None, task_id=None, task=None, retval=None, **extra):
print(f"Task {task.name}[{task_id}] completed: {retval}")
@signals.task_failure.connect
def task_failure_handler(sender=None, task_id=None, exception=None, traceback=None, **extra):
print(f"Task {task_id} failed: {exception}")
@signals.task_retry.connect
def task_retry_handler(sender=None, task_id=None, reason=None, **extra):
()
Flower Monitoring
pip install flower
celery -A myapp flower --port=5555
flower_basic_auth = ['admin:password']
flower_persistent = True
flower_db = 'flower.db'
flower_max_tasks = 10000
Inspecting Workers
from celery_app import app
i = app.control.inspect()
print(i.active())
print(i.scheduled())
print(i.reserved())
print(i.stats())
print(i.registered())
app.control.revoke(task_id, terminate=True)
app.control.shutdown()
app.control.pool_restart()
app.control.rate_limit('myapp.tasks.slow_task', '10/m')
Command Line Inspection
celery -A myapp inspect active
celery -A myapp inspect scheduled
celery -A myapp inspect stats
celery -A myapp inspect registered
celery -A myapp control revoke <task_id>
celery -A myapp control shutdown
celery -A myapp purge
Custom Metrics
@app.task(bind=True)
def tracked_task(self, data):
from prometheus_client import Counter, Histogram
task_counter = Counter('celery_tasks_total', 'Total tasks')
task_duration = Histogram('celery_task_duration_seconds', 'Task duration')
with task_duration.time():
result = process(data)
task_counter.inc()
return result
Framework Integration
Django Integration
import os
from celery import Celery
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
app = Celery('myproject')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
from .celery import app as celery_app
__all__ = ('celery_app',)
CELERY_BROKER_URL = 'redis://localhost:6379/0'
CELERY_RESULT_BACKEND = 'redis://localhost:6379/1'
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TIMEZONE = 'UTC'
CELERY_ENABLE_UTC = True
from celery import shared_task
from django.core.mail import send_mail
@shared_task
def send_email_task(subject, message, recipient):
send_mail(subject, message, 'from@example.com', [recipient])
return f"Email sent to {recipient}"
from myapp.tasks import send_email_task
def my_view(request):
send_email_task.delay('Hello', 'Welcome!', 'user@example.com')
HttpResponse()
FastAPI Integration
from celery import Celery
celery_app = Celery(
'fastapi_app',
broker='redis://localhost:6379/0',
backend='redis://localhost:6379/1'
)
@celery_app.task
def process_data(data: dict):
import time
time.sleep(10)
return {"processed": data, "status": "complete"}
from fastapi import FastAPI, BackgroundTasks
from celery_app import process_data
app = FastAPI()
@app.post("/process")
async def process_endpoint(data: dict):
task = process_data.delay(data)
return {"task_id": task.id, "status": "queued"}
@app.get("/status/{task_id}")
async def check_status(task_id: str):
from celery.result import AsyncResult
task = AsyncResult(task_id, app=celery_app)
{
: task_id,
: task.status,
: task.result task.ready()
}
When to Use Celery vs FastAPI BackgroundTasks:
- FastAPI BackgroundTasks: Simple, fire-and-forget tasks (logging, cleanup)
- Celery: Distributed processing, retries, scheduling, task results
Flask Integration
from celery import Celery
def make_celery(app):
celery = Celery(
app.import_name,
broker=app.config['CELERY_BROKER_URL'],
backend=app.config['CELERY_RESULT_BACKEND']
)
celery.conf.update(app.config)
class ContextTask(celery.Task):
def __call__(self, *args, **kwargs):
with app.app_context():
return self.run(*args, **kwargs)
celery.Task = ContextTask
return celery
from flask import Flask
from celery_app import make_celery
app = Flask(__name__)
app.config['CELERY_BROKER_URL'] = 'redis://localhost:6379/0'
app.config['CELERY_RESULT_BACKEND'] = 'redis://localhost:6379/1'
celery = make_celery(app)
@celery.task
def send_email(to, subject, body):
with app.app_context():
mail.send(Message(subject, recipients=[to], body=body))
@app.route('/send')
def send_route():
send_email.delay('user@example.com', 'Hello', 'Welcome!')
return 'Email queued'
Testing Strategies
Eager Mode (Synchronous Execution)
import pytest
from celery_app import app
@pytest.fixture(scope='session')
def celery_config():
return {
'broker_url': 'memory://',
'result_backend': 'cache+memory://',
'task_always_eager': True,
'task_eager_propagates': True,
}
def test_add_task():
result = add.delay(4, 6)
assert result.get() == 10
def test_task_failure():
with pytest.raises(ValueError):
failing_task.delay()
Testing with Real Broker
import pytest
from celery_app import app
@pytest.fixture(scope='session')
def celery_config():
return {
'broker_url': 'redis://localhost:6379/15',
'result_backend': 'redis://localhost:6379/15',
}
@pytest.fixture
def celery_worker(celery_app):
"""Start worker for tests"""
with celery_app.Worker() as worker:
yield worker
def test_async_task(celery_worker):
result = async_task.delay(data)
assert result.get(timeout=10) == expected
Mocking External Dependencies
from unittest.mock import patch, MagicMock
@app.task
def fetch_and_process(url):
response = requests.get(url)
return process(response.json())
def test_fetch_and_process():
with patch('requests.get') as mock_get:
mock_get.return_value.json.return_value = {'data': 'test'}
result = fetch_and_process.delay('http://example.com')
assert result.get() == expected_result
mock_get.assert_called_once_with('http://example.com')
Testing Periodic Tasks
from celery.schedules import crontab
def test_periodic_task_schedule():
from celery_app import app
schedule = app.conf.beat_schedule['daily-report']
assert schedule['task'] == 'myapp.tasks.daily_report'
assert schedule['schedule'] == crontab(hour=0, minute=0)
def test_periodic_task_execution():
result = daily_report()
assert result['status'] == 'complete'
Integration Testing
import pytest
from celery_app import app
@pytest.fixture(scope='module')
def celery_app():
app.conf.update(
broker_url='redis://localhost:6379/15',
result_backend='redis://localhost:6379/15',
)
return app
@pytest.fixture(scope='module')
def celery_worker(celery_app):
with celery_app.Worker() as worker:
yield worker
def test_workflow(celery_worker):
from celery import chain
workflow = chain(
fetch_data.s(url),
process_data.s(),
save_results.s()
)
result = workflow.apply_async()
output = result.get(timeout=30)
assert output['status'] == 'saved'
Production Patterns
Worker Configuration
celery -A myapp worker \
--autoscale=10,3 \
--max-tasks-per-child=1000 \
--time-limit=300 \
--soft-time-limit=240 \
--loglevel=info \
--logfile=/var/log/celery/worker.log \
--pidfile=/var/run/celery/worker.pid
celery multi start \
worker1 -A myapp -Q high_priority -c 4 --max-tasks-per-child=100 \
worker2 -A myapp -Q default -c 2 --max-tasks-per-child=1000 \
worker3 -A myapp -Q low_priority -c 1 --autoscale=3,1
celery multi stop worker1 worker2 worker3
celery multi stopwait worker1 worker2 worker3
Configuration Best Practices
import os
broker_url = os.getenv('CELERY_BROKER_URL', 'redis://localhost:6379/0')
broker_connection_retry_on_startup = True
broker_pool_limit = 50
result_backend = os.getenv('CELERY_RESULT_BACKEND', 'redis://localhost:6379/1')
result_expires = 3600
task_serializer = 'json'
result_serializer = 'json'
accept_content = ['json']
timezone = 'UTC'
enable_utc = True
worker_prefetch_multiplier = 4
worker_max_tasks_per_child = 1000
task_acks_late = True
task_reject_on_worker_lost = True
task_track_started = True
task_time_limit = 300
task_soft_time_limit = 240
worker_log_format = '[%(asctime)s: %(levelname)s/%(processName)s] %(message)s'
worker_task_log_format = '[%(asctime)s: %(levelname)s/%(processName)s][%(task_name)s(%(task_id)s)] %(message)s'
Systemd Service
[Unit]
Description=Celery Service
After=network.target redis.target
[Service]
Type=forking
User=celery
Group=celery
WorkingDirectory=/opt/myapp
Environment="PATH=/opt/myapp/venv/bin"
ExecStart=/opt/myapp/venv/bin/celery multi start worker1 \
-A myapp \
--pidfile=/var/run/celery/%n.pid \
--logfile=/var/log/celery/%n%I.log \
--loglevel=INFO
ExecStop=/opt/myapp/venv/bin/celery multi stopwait worker1 \
--pidfile=/var/run/celery/%n.pid
ExecReload=/opt/myapp/venv/bin/celery multi restart worker1 \
-A myapp \
--pidfile=/var/run/celery/%n.pid \
--logfile=/var/log/celery/%n%I.log \
--loglevel=INFO
Restart=always
[Install]
WantedBy=multi-user.target
[Unit]
Description=Celery Beat Service
After=network.target redis.target
[Service]
Type=simple
User=celery
Group=celery
WorkingDirectory=/opt/myapp
Environment="PATH=/opt/myapp/venv/bin"
ExecStart=/opt/myapp/venv/bin/celery -A myapp beat \
--loglevel=INFO \
--pidfile=/var/run/celery/beat.pid
Restart=always
[Install]
WantedBy=multi-user.target
Sentry Integration
pip install sentry-sdk
import sentry_sdk
from sentry_sdk.integrations.celery import CeleryIntegration
sentry_sdk.init(
dsn="https://your-sentry-dsn",
integrations=[CeleryIntegration()],
traces_sample_rate=0.1,
)
@app.task
def my_task(x):
return risky_operation(x)
Rate Limiting
app.conf.task_default_rate_limit = '100/m'
@app.task(rate_limit='10/m')
def rate_limited_task(x):
return expensive_operation(x)
app.control.rate_limit('myapp.tasks.slow_task', '5/m')
@app.task(rate_limit='10/s')
def api_call(endpoint):
return requests.get(endpoint)
Health Checks
from celery_app import app
def check_celery_health():
"""Health check endpoint"""
try:
i = app.control.inspect()
stats = i.stats()
if not stats:
return {'status': 'unhealthy', 'reason': 'No workers available'}
result = app.control.ping(timeout=1.0)
if not result:
return {'status': 'unhealthy', 'reason': 'Workers not responding'}
return {'status': 'healthy', 'workers': len(stats)}
except Exception as e:
return {'status': 'unhealthy', 'error': str(e)}
@app.get("/health/celery")
async def celery_health():
return check_celery_health()
Performance Optimization
Task Optimization
@app.task(ignore_result=True)
def send_notification(user_id, message):
notify(user_id, message)
@app.task(compression='gzip')
def process_large_data(data):
return analyze(data)
@app.task(serializer='msgpack')
def fast_task(data):
return process(data)
Worker Tuning
worker_concurrency = 4
worker_concurrency = 20
worker_prefetch_multiplier = 4
task_acks_late = True
task_acks_late = False
worker_max_tasks_per_child = 1000
worker_max_memory_per_child = 200000
Database Result Backend Optimization
result_backend = 'redis://localhost:6379/1'
result_backend = 'db+postgresql://user:pass@localhost/celery'
database_engine_options = {
'pool_size': 20,
'pool_recycle': 3600,
}
result_expires = 3600
Task Chunking
from celery import group
for item in large_list:
process_item.delay(item)
def chunks(lst, n):
for i in range(0, len(lst), n):
yield lst[i:i + n]
@app.task
def process_batch(items):
return [process_item(item) for item in items]
job = group(process_batch.s(chunk) for chunk in chunks(large_list, 100))
result = job.apply_async()
Connection Pooling
broker_pool_limit = 50
redis_max_connections = 50
from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool
engine = create_engine(
'postgresql://user:pass@localhost/db',
poolclass=QueuePool,
pool_size=20,
max_overflow=0,
)
Common Use Cases
Email Sending
@app.task(bind=True, max_retries=3)
def send_email_task(self, to, subject, body, attachments=None):
try:
msg = EmailMessage(subject, body, 'from@example.com', [to])
if attachments:
for filename, content, mimetype in attachments:
msg.attach(filename, content, mimetype)
msg.send()
return {'status': 'sent', 'to': to}
except SMTPException as exc:
raise self.retry(exc=exc, countdown=60 * (2 ** self.request.retries))
@app.task(rate_limit='100/m')
def send_bulk_email(recipients, subject, template):
for recipient in recipients:
send_email_task.delay(recipient, subject, render_template(template, recipient))
Report Generation
@app.task(bind=True, time_limit=600)
def generate_report(self, report_type, user_id, start_date, end_date):
self.update_state(state='PROGRESS', meta={'current': 0, 'total': 100})
data = fetch_report_data(report_type, start_date, end_date)
self.update_state(state='PROGRESS', meta={'current': 30, 'total': 100})
pdf = render_pdf(data)
self.update_state(state='PROGRESS', meta={'current': 70, 'total': 100})
url = upload_to_s3(pdf, f'reports/{user_id}/{report_type}.pdf')
self.update_state(state='PROGRESS', meta={'current': 90, 'total': 100})
send_email_task.delay(
get_user_email(user_id),
'Report Ready',
f'Your report is ready: {url}'
)
return {'status': 'complete', 'url': url}
from celery.result AsyncResult
task = AsyncResult(task_id)
task.state == :
(task.info)
Data Processing Pipeline
from celery import chain, group
@app.task
def fetch_data(source):
return download(source)
@app.task
def clean_data(raw_data):
return clean(raw_data)
@app.task
def transform_data(clean_data):
return transform(clean_data)
@app.task
def load_data(transformed_data):
save_to_database(transformed_data)
return {'status': 'loaded', 'rows': len(transformed_data)}
etl_pipeline = chain(
fetch_data.s('https://api.example.com/data'),
clean_data.s(),
transform_data.s(),
load_data.s()
)
result = etl_pipeline.apply_async()
Webhook Processing
@app.task(bind=True, autoretry_for=(RequestException,), max_retries=5)
def process_webhook(self, webhook_data):
if not verify_signature(webhook_data):
raise ValueError("Invalid signature")
event_type = webhook_data['type']
if event_type == 'payment.success':
update_order_status(webhook_data['order_id'], 'paid')
send_confirmation_email.delay(webhook_data['customer_email'])
elif event_type == 'payment.failed':
notify_admin.delay('Payment Failed', webhook_data)
return {'status': 'processed', 'event': event_type}
@app.post("/webhooks/stripe")
async def stripe_webhook(request: Request):
data = await request.json()
process_webhook.delay(data)
return {"status": "queued"}
Image Processing
from celery import group, chord
@app.task
def resize_image(image_path, size):
from PIL import Image
img = Image.open(image_path)
img.thumbnail(size)
output_path = f"{image_path}_{size[0]}x{size[1]}.jpg"
img.save(output_path)
return output_path
@app.task
def upload_to_cdn(image_paths):
urls = []
for path in image_paths:
url = cdn_upload(path)
urls.append(url)
return urls
def process_uploaded_image(image_path):
sizes = [(800, 600), (400, 300), (200, 150), (100, 100)]
workflow = chord([
resize_image.s(image_path, size) for size in sizes
])(upload_to_cdn.s())
return workflow.apply_async()
Alternatives Comparison
Celery vs RQ (Redis Queue)
RQ: Simpler Redis-only task queue
When to use RQ:
- Simple use case (no routing, basic retries)
- Redis-only infrastructure
- Python 3 only
- Smaller scale (<1000 tasks/min)
When to use Celery:
- Complex workflows (chains, chords)
- Multiple broker options
- Advanced routing and priorities
- Large scale (>1000 tasks/min)
- Periodic tasks
from redis import Redis
from rq import Queue
redis_conn = Redis()
q = Queue(connection=redis_conn)
job = q.enqueue(my_function, arg1, arg2)
result = job.result
Celery vs Huey
Huey: Lightweight task queue with minimal dependencies
When to use Huey:
- Small to medium projects
- Minimal configuration
- Redis or in-memory only
- Simple periodic tasks
When to use Celery:
- Enterprise-scale applications
- Complex task dependencies
- Multiple broker/backend options
- Advanced monitoring needs
from huey import RedisHuey
huey = RedisHuey('myapp')
@huey.task()
def add(a, b):
return a + b
result = add(1, 2)
Celery vs Dramatiq
Dramatiq: Modern alternative focusing on reliability
When to use Dramatiq:
- Reliability over features
- Simpler API
- Better type hints
- RabbitMQ or Redis
When to use Celery:
- Mature ecosystem
- More broker options
- Canvas workflows
- Larger community
import dramatiq
@dramatiq.actor
def add(x, y):
return x + y
add.send(1, 2)
Celery vs Cloud Services
AWS Lambda, Google Cloud Functions, Azure Functions
When to use Cloud Functions:
- Serverless infrastructure
- Event-driven workflows
- Pay-per-execution model
- Auto-scaling
When to use Celery:
- Self-hosted infrastructure
- Complex task workflows
- Cost predictability
- Full control over execution
Best Practices
Task Design
-
Idempotency: Tasks should be safe to run multiple times
@app.task
def process_order(order_id):
order = Order.objects.get(id=order_id)
if order.status == 'processed':
return
order.process()
order.status = 'processed'
order.save()
-
Small, Focused Tasks: One responsibility per task
@app.task
def process_user(user_id):
send_welcome_email(user_id)
create_profile(user_id)
setup_notifications(user_id)
@app.task
def send_welcome_email(user_id):
...
@app.task
def create_profile(user_id):
...
workflow = group([
send_welcome_email.s(user_id),
create_profile.s(user_id),
setup_notifications.s(user_id)
])
-
Avoid Database Objects in Arguments: Use IDs instead
@app.task
def process_user(user):
...
@app.task
def process_user(user_id):
user = User.objects.get(id=user_id)
...
-
Set Time Limits: Prevent runaway tasks
Error Handling
- Categorize Exceptions: Different handling for different errors
- Use Exponential Backoff: Avoid overwhelming failing services
- Set Max Retries: Don't retry forever
- Log Failures: Always log why tasks fail
Performance
- Use
ignore_result=True: For tasks that don't need results
- Batch Operations: Process multiple items per task
- Optimize Serialization: Use msgpack for speed
- Connection Pooling: Reuse database/broker connections
- Task Chunking: Avoid creating millions of tiny tasks
Monitoring
- Enable Events: Track task lifecycle
- Use Flower: Web-based monitoring
- Health Checks: Monitor worker availability
- Sentry Integration: Track errors
Security
- Validate Input: Always validate task arguments
- Secure Broker: Use authentication and encryption
- Limit Task Execution Time: Prevent resource exhaustion
- Rate Limiting: Protect against task flooding
Troubleshooting
Tasks Not Executing
Symptoms: Tasks queued but not processing
Diagnosis:
celery -A myapp inspect active
celery -A myapp inspect stats
celery -A myapp inspect registered
Solutions:
- Start workers:
celery -A myapp worker
- Check worker is consuming correct queues
- Verify task routing configuration
- Check broker connectivity
Tasks Failing Silently
Symptoms: Tasks show SUCCESS but don't work
Diagnosis:
app.conf.task_track_started = True
result = task.delay()
if result.failed():
print(result.traceback)
Solutions:
- Check logs:
celery -A myapp worker --loglevel=debug
- Enable eager mode in tests to see exceptions
- Use
task_eager_propagates = True in tests
Memory Leaks
Symptoms: Worker memory grows over time
Solutions:
worker_max_tasks_per_child = 1000
worker_max_memory_per_child = 200000
Slow Task Execution
Symptoms: Tasks taking longer than expected
Diagnosis:
import time
@app.task(bind=True)
def timed_task(self):
start = time.time()
result = slow_operation()
duration = time.time() - start
logger.info(f"Task {self.request.id} took {duration}s")
return result
Solutions:
- Increase worker concurrency
- Optimize task code
- Use task chunking
- Add more workers
Broker Connection Issues
Symptoms: Tasks not reaching workers
Diagnosis:
python -c "from celery_app import app; print(app.connection().connect())"
Solutions:
- Check broker is running:
redis-cli ping or rabbitmqctl status
- Verify broker URL in configuration
- Check network connectivity
- Enable connection retry:
broker_connection_retry_on_startup = True
Task Results Not Persisting
Symptoms: result.get() returns None
Solutions:
- Verify result backend configured
- Check task doesn't have
ignore_result=True
- Verify result hasn't expired (
result_expires)
- Test backend connection
Beat Not Scheduling Tasks
Symptoms: Periodic tasks not running
Diagnosis:
ps aux | grep celery | grep beat
celery -A myapp inspect scheduled
Solutions:
- Ensure beat process is running
- Verify
beat_schedule configuration
- Check beat log for errors
- Use database scheduler for dynamic schedules
Worker Crashes
Symptoms: Workers die unexpectedly
Solutions:
- Check logs for errors
- Set
worker_max_tasks_per_child to prevent memory leaks
- Add task time limits
- Use systemd for automatic restart
- Monitor with Flower
Task Queue Buildup
Symptoms: Tasks accumulating in queue
Solutions:
- Add more workers
- Increase worker concurrency
- Optimize slow tasks
- Add task routing to distribute load
- Check for blocked workers
Advanced Configuration
Custom Task Classes
from celery import Task
class DatabaseTask(Task):
"""Task that manages database connections"""
_db = None
@property
def db(self):
if self._db is None:
self._db = create_db_connection()
return self._db
def after_return(self, status, retval, task_id, args, kwargs, einfo):
"""Close connection after task"""
if self._db is not None:
self._db.close()
@app.task(base=DatabaseTask)
def db_task(query):
return db_task.db.execute(query)
Custom Serializers
from kombu.serialization import register
def my_encoder(obj):
return json.dumps(obj)
def my_decoder(data):
return json.loads(data)
register('myjson', my_encoder, my_decoder,
content_type='application/x-myjson',
content_encoding='utf-8')
app.conf.task_serializer = 'myjson'
Task Inheritance
class BaseTask(Task):
def on_failure(self, exc, task_id, args, kwargs, einfo):
send_alert(f"Task {self.name} failed", str(exc))
def on_retry(self, exc, task_id, args, kwargs, einfo):
logger.warning(f"Task {self.name} retrying")
@app.task(base=BaseTask)
def monitored_task():
return perform_work()
End of Celery Skill Documentation
For more information:
Related Skills
When using Celery, these skills enhance your workflow:
- django: Django + Celery integration for background tasks
- fastapi-local-dev: FastAPI + Celery patterns for async API operations
- test-driven-development: Testing async tasks and task chains
- systematic-debugging: Debugging distributed task failures and race conditions
[Full documentation available in these skills if deployed in your bundle]