Celery Cheatsheet
Section titled “Celery Cheatsheet”Celery is a distributed task queue for running work outside the request/response path. A producer sends a task message to a broker, workers consume messages, and an optional result backend stores task state and return values.
Core Concepts
Section titled “Core Concepts”| Term | Meaning |
|---|---|
| Producer | Your web app, CLI, cron job, or service that sends tasks |
| Broker | Message transport such as Redis or RabbitMQ |
| Worker | Process that executes tasks from queues |
| Queue | Named message stream workers consume from |
| Task | Python callable registered with Celery |
| Result backend | Storage for task state/results such as Redis, database, or RPC |
| Beat | Scheduler that sends periodic tasks |
| Canvas | Primitives for workflows: chains, groups, chords, maps |
Web/API process -> broker -> worker process -> optional result backendInstallation
Section titled “Installation”# Base installpip install celery
# Redis broker/backend supportpip install "celery[redis]"
# RabbitMQ uses AMQP support included through Kombu dependenciespip install celery
# Common extraspip install flower # web monitoring UIpip install django-celery-beat # DB-backed periodic tasks for Djangopip install django-celery-resultsMinimal App
Section titled “Minimal App”Create proj/celery_app.py:
from celery import Celery
app = Celery( "proj", 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.taskdef add(x, y): return x + yRun a worker:
celery -A proj.celery_app worker --loglevel=INFOCall a task:
from proj.celery_app import add
result = add.delay(2, 3)print(result.id)print(result.get(timeout=10))Project Layout
Section titled “Project Layout”proj/ __init__.py celery_app.py tasks.pyproj/celery_app.py:
from celery import Celery
app = Celery("proj")app.config_from_object("proj.celeryconfig")app.autodiscover_tasks(["proj"])proj/tasks.py:
from .celery_app import app
@app.task(name="proj.tasks.send_email")def send_email(user_id): ...proj/celeryconfig.py:
broker_url = "redis://localhost:6379/0"result_backend = "redis://localhost:6379/1"timezone = "UTC"task_serializer = "json"accept_content = ["json"]result_serializer = "json"Command Line
Section titled “Command Line”Workers
Section titled “Workers”# Start workercelery -A proj.celery_app worker --loglevel=INFO
# Set concurrencycelery -A proj.celery_app worker --concurrency=4
# Consume specific queuescelery -A proj.celery_app worker -Q default,email,high_priority
# Name a workercelery -A proj.celery_app worker -n worker1@%h
# Enable task events for monitoringcelery -A proj.celery_app worker -E
# Use a poolcelery -A proj.celery_app worker --pool=preforkcelery -A proj.celery_app worker --pool=threads --concurrency=20celery -A proj.celery_app worker --pool=soloBeat Scheduler
Section titled “Beat Scheduler”# Start schedulercelery -A proj.celery_app beat --loglevel=INFO
# Development only: worker + beat in one processcelery -A proj.celery_app worker -B --loglevel=INFOInspect and Control
Section titled “Inspect and Control”# Check live workerscelery -A proj.celery_app status
# Registered taskscelery -A proj.celery_app inspect registered
# Active, reserved, scheduled taskscelery -A proj.celery_app inspect activecelery -A proj.celery_app inspect reservedcelery -A proj.celery_app inspect scheduled
# Worker statscelery -A proj.celery_app inspect stats
# Revoke a taskcelery -A proj.celery_app control revoke <task-id>
# Revoke and terminate running taskcelery -A proj.celery_app control revoke <task-id> --terminate
# Shut down workerscelery -A proj.celery_app control shutdownDefining Tasks
Section titled “Defining Tasks”Basic Task
Section titled “Basic Task”@app.taskdef resize_image(image_id): ...Named Task
Section titled “Named Task”@app.task(name="images.resize")def resize_image(image_id): ...Bound Task
Section titled “Bound Task”Use bind=True when the task needs self.request, retries, logging context, or custom state.
@app.task(bind=True)def process_order(self, order_id): print(self.request.id) print(self.request.retries)Task Options
Section titled “Task Options”@app.task( name="reports.generate", bind=True, max_retries=3, default_retry_delay=60, soft_time_limit=300, time_limit=360, ignore_result=False,)def generate_report(self, report_id): ...| Option | Purpose |
|---|---|
name | Stable task name used on the broker |
bind=True | Pass task instance as first argument |
max_retries | Maximum retry attempts |
default_retry_delay | Delay between retries in seconds |
autoretry_for | Exception tuple that triggers automatic retry |
retry_backoff | Exponential retry delay |
retry_jitter | Randomize retry delay to reduce spikes |
acks_late | Acknowledge only after task finishes |
ignore_result | Do not store result |
soft_time_limit | Raise exception inside task after limit |
time_limit | Kill task after hard limit |
rate_limit | Limit execution rate, such as 10/m |
Calling Tasks
Section titled “Calling Tasks”# Shortcut: send immediatelytask.delay(arg1, arg2, key="value")
# Full APItask.apply_async(args=[arg1, arg2], kwargs={"key": "value"})
# Delay executiontask.apply_async(args=[42], countdown=60)
# Run at exact timetask.apply_async(args=[42], eta=datetime.utcnow() + timedelta(minutes=10))
# Expire if not executed in timetask.apply_async(args=[42], expires=300)
# Send to specific queuetask.apply_async(args=[42], queue="high_priority")
# Set priority when broker supports ittask.apply_async(args=[42], priority=9)
# Custom task idtask.apply_async(args=[42], task_id="order-123")Results
Section titled “Results”res = add.delay(2, 3)
res.idres.status # PENDING, STARTED, SUCCESS, FAILURE, RETRY, REVOKEDres.ready()res.successful()res.failed()res.get(timeout=10)res.result # return value or exceptionres.tracebackCommon result methods:
| Method | Description |
|---|---|
get(timeout=n) | Wait for result |
ready() | Task finished or failed |
successful() | Task completed successfully |
failed() | Task failed |
forget() | Delete stored result |
revoke() | Ask workers not to execute task |
Avoid waiting for Celery results inside web requests unless the wait is short and intentional. It often defeats the point of using a queue.
Retries
Section titled “Retries”Manual Retry
Section titled “Manual Retry”@app.task(bind=True, max_retries=5, default_retry_delay=30)def fetch_url(self, url): try: return requests.get(url, timeout=10).text except requests.RequestException as exc: raise self.retry(exc=exc)Automatic Retry
Section titled “Automatic Retry”@app.task( autoretry_for=(requests.RequestException,), retry_backoff=True, retry_backoff_max=600, retry_jitter=True, max_retries=5,)def fetch_url(url): return requests.get(url, timeout=10).textRetry with Custom Countdown
Section titled “Retry with Custom Countdown”@app.task(bind=True)def sync_customer(self, customer_id): try: ... except RateLimited as exc: raise self.retry(exc=exc, countdown=120)Idempotency and Reliability
Section titled “Idempotency and Reliability”Tasks should be safe to run more than once. Workers can crash after side effects, brokers can redeliver messages, and retries can duplicate work.
@app.task(bind=True, acks_late=True)def charge_invoice(self, invoice_id): invoice = Invoice.objects.get(id=invoice_id)
if invoice.status == "paid": return "already-paid"
# Use an idempotency key with the external API. payment.charge( invoice.customer_id, amount=invoice.total, idempotency_key=f"invoice:{invoice.id}", )
invoice.status = "paid" invoice.save(update_fields=["status"])Production reliability settings:
task_acks_late = Truetask_reject_on_worker_lost = Trueworker_prefetch_multiplier = 1task_track_started = True| Setting | Why it matters |
|---|---|
task_acks_late=True | Redeliver task if worker dies before finishing |
task_reject_on_worker_lost=True | Reject instead of ack when worker process disappears |
worker_prefetch_multiplier=1 | Prevent one worker from reserving too many tasks |
task_track_started=True | Report STARTED state |
Time Limits
Section titled “Time Limits”@app.task(soft_time_limit=60, time_limit=75)def import_file(file_id): ...from celery.exceptions import SoftTimeLimitExceeded
@app.task(soft_time_limit=60, time_limit=75)def import_file(file_id): try: ... except SoftTimeLimitExceeded: cleanup_temp_files(file_id) raise| Limit | Behavior |
|---|---|
soft_time_limit | Raises SoftTimeLimitExceeded inside the task |
time_limit | Terminates the worker child process |
Routing and Queues
Section titled “Routing and Queues”Simple Routing
Section titled “Simple Routing”task_default_queue = "default"
task_routes = { "emails.*": {"queue": "email"}, "reports.*": {"queue": "reports"}, "images.resize": {"queue": "media"},}Start workers for specific queues:
celery -A proj.celery_app worker -Q defaultcelery -A proj.celery_app worker -Q email,reportscelery -A proj.celery_app worker -Q media --concurrency=2Send directly:
send_email.apply_async(args=[user_id], queue="email")Advanced Queue Declaration
Section titled “Advanced Queue Declaration”from kombu import Exchange, Queue
task_queues = ( Queue("default", Exchange("default"), routing_key="default"), Queue("email", Exchange("email"), routing_key="email.#"), Queue("reports", Exchange("reports"), routing_key="reports.#"),)
task_default_exchange = "default"task_default_routing_key = "default"Periodic Tasks
Section titled “Periodic Tasks”Static Beat Schedule
Section titled “Static Beat Schedule”from celery.schedules import crontab
beat_schedule = { "cleanup-every-hour": { "task": "maintenance.cleanup", "schedule": 3600.0, "args": (), }, "send-daily-digest": { "task": "emails.daily_digest", "schedule": crontab(hour=8, minute=0), }, "sync-every-weekday": { "task": "sync.customers", "schedule": crontab(minute=0, hour="*/2", day_of_week="mon-fri"), },}Run beat:
celery -A proj.celery_app beat --loglevel=INFORun workers separately:
celery -A proj.celery_app worker --loglevel=INFODynamic Schedules in Django
Section titled “Dynamic Schedules in Django”pip install django-celery-beatpython manage.py migrateINSTALLED_APPS = [ ... "django_celery_beat",]celery -A config beat -S django --loglevel=INFOCanvas Workflows
Section titled “Canvas Workflows”Canvas lets you compose tasks into workflows.
Signatures
Section titled “Signatures”sig = add.s(2, 3)sig.delay()sig.apply_async()Run tasks one after another. The previous result is passed to the next task.
from celery import chain
workflow = chain(add.s(2, 3), multiply.s(10), store_result.s())workflow.delay()
# Pipe syntax(add.s(2, 3) | multiply.s(10) | store_result.s()).delay()Run tasks in parallel.
from celery import group
job = group(fetch_url.s(url) for url in urls)result = job.delay()values = result.get(timeout=30)Run a callback after a group finishes.
from celery import chord
workflow = chord( group(fetch_url.s(url) for url in urls), summarize_results.s(),)workflow.delay()Immutable Signatures
Section titled “Immutable Signatures”Use .si() when you do not want previous results injected.
(create_user.s(email) | send_welcome_email.si(email)).delay()Worker Pools
Section titled “Worker Pools”| Pool | Use case |
|---|---|
prefork | Default for CPU-bound or normal Python tasks |
threads | I/O-heavy tasks when libraries are thread-safe |
solo | Debugging, Windows development, simple local runs |
eventlet | Greenlet-based I/O with monkey patching |
gevent | Greenlet-based I/O with monkey patching |
celery -A proj.celery_app worker --pool=prefork --concurrency=4celery -A proj.celery_app worker --pool=threads --concurrency=20celery -A proj.celery_app worker --pool=soloGeneral guidance:
| Workload | Recommendation |
|---|---|
| CPU-heavy | prefork, fewer processes, separate queue |
| Network I/O | threads, gevent, or eventlet after testing |
| Long-running tasks | Dedicated queue and worker_prefetch_multiplier=1 |
| Memory leaks | Use --max-tasks-per-child or --max-memory-per-child |
Concurrency and Prefetch
Section titled “Concurrency and Prefetch”# Four concurrent child processes or threadscelery -A proj.celery_app worker --concurrency=4
# Restart child process after N taskscelery -A proj.celery_app worker --max-tasks-per-child=1000
# Restart child process after memory limit in KBcelery -A proj.celery_app worker --max-memory-per-child=300000# Reserve one task per worker slotworker_prefetch_multiplier = 1
# Useful for long-running taskstask_acks_late = TruePrefetch behavior:
| Setting | Effect |
|---|---|
worker_prefetch_multiplier=4 | Each worker process may reserve 4 tasks |
worker_prefetch_multiplier=1 | Fairer distribution for long tasks |
worker_prefetch_multiplier=0 | Worker may consume as many messages as possible |
Monitoring
Section titled “Monitoring”Flower
Section titled “Flower”celery -A proj.celery_app flower
# Custom portcelery -A proj.celery_app flower --port=5555
# Basic authcelery -A proj.celery_app flower --basic-auth=user:passwordEvents
Section titled “Events”# Enable events on workercelery -A proj.celery_app worker -E
# Terminal event monitorcelery -A proj.celery_app eventsLogging
Section titled “Logging”import loggingfrom celery.utils.log import get_task_logger
logger = get_task_logger(__name__)
@app.task(bind=True)def rebuild_index(self): logger.info("starting rebuild", extra={"task_id": self.request.id})Custom State
Section titled “Custom State”@app.task(bind=True)def import_rows(self, file_id): total = 1000 for current in range(total): ... self.update_state( state="PROGRESS", meta={"current": current, "total": total}, )Broker and Backend URLs
Section titled “Broker and Backend URLs”broker_url = "redis://localhost:6379/0"result_backend = "redis://localhost:6379/1"
# With passwordbroker_url = "redis://:password@localhost:6379/0"
# TLSbroker_url = "rediss://:password@redis.example.com:6379/0"RabbitMQ
Section titled “RabbitMQ”broker_url = "amqp://guest:guest@localhost:5672//"
# Virtual hostbroker_url = "amqp://user:password@rabbitmq.example.com:5672/myvhost"Database Result Backend
Section titled “Database Result Backend”pip install sqlalchemyresult_backend = "db+postgresql://user:password@localhost/celery_results"Serialization
Section titled “Serialization”task_serializer = "json"result_serializer = "json"accept_content = ["json"]timezone = "UTC"enable_utc = TrueAvoid pickle unless every producer, worker, and broker is trusted.
# Do not enable this casuallytask_serializer = "pickle"accept_content = ["pickle"]Pass simple data:
# Goodsend_email.delay(user_id=42)
# Risky: large, stale, or unserializable objectsend_email.delay(user_object)Django Setup
Section titled “Django Setup”config/celery.py:
import osfrom celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
app = Celery("config")app.config_from_object("django.conf:settings", namespace="CELERY")app.autodiscover_tasks()config/__init__.py:
from .celery import app as celery_app
__all__ = ("celery_app",)settings.py:
CELERY_BROKER_URL = "redis://localhost:6379/0"CELERY_RESULT_BACKEND = "redis://localhost:6379/1"CELERY_TASK_SERIALIZER = "json"CELERY_ACCEPT_CONTENT = ["json"]CELERY_RESULT_SERIALIZER = "json"CELERY_TIMEZONE = "UTC"CELERY_TASK_TRACK_STARTED = Trueapp/tasks.py:
from celery import shared_task
@shared_task(bind=True)def send_invoice(self, invoice_id): ...Run:
celery -A config worker --loglevel=INFOcelery -A config beat --loglevel=INFODjango Transactions
Section titled “Django Transactions”Queue tasks after the database transaction commits.
from django.db import transaction
def create_order(request): order = Order.objects.create(...)
transaction.on_commit( lambda: send_order_email.delay(order.id) )FastAPI Setup
Section titled “FastAPI Setup”worker.py:
from celery import Celery
celery_app = Celery( "worker", broker="redis://localhost:6379/0", backend="redis://localhost:6379/1",)
@celery_app.taskdef send_email(email: str): ...main.py:
from fastapi import FastAPIfrom worker import send_email
app = FastAPI()
@app.post("/send")def send(email: str): result = send_email.delay(email) return {"task_id": result.id}Run:
uvicorn main:app --reloadcelery -A worker.celery_app worker --loglevel=INFODocker Compose
Section titled “Docker Compose”services: redis: image: redis:7 ports: - "6379:6379"
web: build: . command: uvicorn main:app --host 0.0.0.0 --port 8000 environment: CELERY_BROKER_URL: redis://redis:6379/0 CELERY_RESULT_BACKEND: redis://redis:6379/1 depends_on: - redis
worker: build: . command: celery -A worker.celery_app worker --loglevel=INFO environment: CELERY_BROKER_URL: redis://redis:6379/0 CELERY_RESULT_BACKEND: redis://redis:6379/1 depends_on: - redis
beat: build: . command: celery -A worker.celery_app beat --loglevel=INFO environment: CELERY_BROKER_URL: redis://redis:6379/0 depends_on: - redisTesting
Section titled “Testing”Eager Mode
Section titled “Eager Mode”task_always_eager = Truetask_eager_propagates = Truedef test_add(): assert add.delay(2, 3).get() == 5Unit Test Task Logic
Section titled “Unit Test Task Logic”Keep complex business logic in normal functions and call those from tasks.
def build_invoice(invoice_id): ...
@app.taskdef build_invoice_task(invoice_id): return build_invoice(invoice_id)def test_build_invoice(): assert build_invoice(invoice.id).total == 100Mock Task Dispatch
Section titled “Mock Task Dispatch”def test_view_queues_task(mocker, client): mocked = mocker.patch("app.views.send_email.delay") client.post("/signup", data={...}) mocked.assert_called_once()Production Settings
Section titled “Production Settings”broker_connection_retry_on_startup = Trueworker_prefetch_multiplier = 1task_acks_late = Truetask_reject_on_worker_lost = Truetask_track_started = Truetask_time_limit = 600task_soft_time_limit = 540result_expires = 3600worker_max_tasks_per_child = 1000worker_send_task_events = Truetask_send_sent_event = True| Setting | Typical reason |
|---|---|
broker_connection_retry_on_startup | Keep retrying broker connection during deploy/startup |
result_expires | Prevent old result buildup |
worker_max_tasks_per_child | Limit memory growth |
worker_send_task_events | Enable monitoring events |
task_send_sent_event | Emit task-sent events from producers |
Security
Section titled “Security”| Practice | Why |
|---|---|
| Use JSON serialization | Avoid arbitrary code execution risk from pickle |
| Protect broker network access | Anyone with broker write access can enqueue tasks |
| Use TLS for remote brokers | Prevent credential and payload exposure |
| Do not pass secrets as args | Task args may appear in logs, events, and results |
| Set result expiration | Avoid storing sensitive task output indefinitely |
| Restrict Flower | It can expose task names, args, states, and worker controls |
task_serializer = "json"accept_content = ["json"]result_expires = 3600Common Patterns
Section titled “Common Patterns”Fire-and-Forget Task
Section titled “Fire-and-Forget Task”@app.task(ignore_result=True)def send_metric(event): ...Dedicated Queue for Slow Tasks
Section titled “Dedicated Queue for Slow Tasks”task_routes = { "reports.generate": {"queue": "slow"},}worker_prefetch_multiplier = 1celery -A proj.celery_app worker -Q slow --concurrency=2Rate-Limited Task
Section titled “Rate-Limited Task”@app.task(rate_limit="100/m")def call_partner_api(payload): ...Singleton-Like Task with Redis Lock
Section titled “Singleton-Like Task with Redis Lock”import redis
r = redis.Redis.from_url("redis://localhost:6379/2")
@app.task(bind=True)def rebuild_cache(self): lock = r.lock("lock:rebuild-cache", timeout=600, blocking_timeout=1) if not lock.acquire(): return "already-running"
try: ... finally: lock.release()Chunk Large Work
Section titled “Chunk Large Work”@app.taskdef process_batch(ids): for item_id in ids: process_one(item_id)
for batch in chunks(all_ids, 500): process_batch.delay(batch)Troubleshooting
Section titled “Troubleshooting”| Symptom | Checks |
|---|---|
Task stuck as PENDING | Worker not running, wrong broker URL, no result backend, task not imported |
Received unregistered task | Worker did not import module, wrong -A, missing autodiscovery |
| Task runs twice | Retry/redelivery, worker crash, non-idempotent task, late ack behavior |
| Worker reserves too much work | Set worker_prefetch_multiplier=1 |
| Task never reaches queue | Broker down, producer using wrong app, routing to unconsumed queue |
| Queue grows forever | Not enough workers, slow task, worker errors, bad routing |
| Result backend fills up | Set result_expires, use ignore_result=True |
| Periodic task runs twice | More than one beat scheduler active |
| Django task cannot find DB row | Use transaction.on_commit() |
| Memory keeps growing | Add worker_max_tasks_per_child or fix leaks |
Useful commands:
celery -A proj.celery_app statuscelery -A proj.celery_app inspect activecelery -A proj.celery_app inspect reservedcelery -A proj.celery_app inspect scheduledcelery -A proj.celery_app inspect registeredcelery -A proj.celery_app reportRedis queue inspection:
redis-cli LLEN celeryredis-cli LRANGE celery 0 5RabbitMQ queue inspection:
rabbitmqctl list_queues name messages consumersrabbitmqctl list_exchangesrabbitmqctl list_bindingsQuick Reference
Section titled “Quick Reference”# Workercelery -A proj.celery_app worker -l INFO
# Worker for queuescelery -A proj.celery_app worker -Q default,email -l INFO
# Beatcelery -A proj.celery_app beat -l INFO
# Flowercelery -A proj.celery_app flower --port=5555
# Statuscelery -A proj.celery_app status
# Inspectcelery -A proj.celery_app inspect activecelery -A proj.celery_app inspect stats
# Revokecelery -A proj.celery_app control revoke <task-id># Send nowtask.delay(*args, **kwargs)
# Send with optionstask.apply_async(args=[...], kwargs={...}, countdown=60, queue="email")
# Retryraise self.retry(exc=exc, countdown=30)
# Chain(task1.s() | task2.s() | task3.s()).delay()
# Groupgroup(task.s(x) for x in items).delay()
# Chordchord(group(task.s(x) for x in items), callback.s()).delay()