Learn how to use Celery with Python and Redis to run background jobs, schedule tasks, and keep your web app responsive under load.
Web requests should finish quickly. When your Python application needs to send emails, process uploads, generate reports, or call slow external APIs, doing that work inside the request cycle creates timeouts, frustrated users, and fragile deployments.
Celery is the most widely used distributed task queue for Python. It lets you offload work to background workers that run separately from your web server, with support for retries, scheduling, and horizontal scaling.
When You Need a Task Queue
Reach for Celery when:
- A task takes more than a few hundred milliseconds and the user does not need the result immediately.
- Work should survive web process restarts (send email after checkout, not during).
- You need to retry failed operations with backoff.
- Multiple workers should process jobs in parallel.
- Tasks must run on a schedule (nightly reports, data cleanup).
Skip Celery when a simple threading call or asyncio.create_task handles the load, or when you only have a handful of jobs per day and cron scripts suffice.
Core Architecture
Celery has three main parts:
- Producer — your web app (Flask, Django, FastAPI) that sends tasks to the queue.
- Broker — a message transport (Redis or RabbitMQ) that holds tasks until workers pick them up.
- Worker — a separate process that executes tasks and stores results (optionally in Redis or the database).
Web App → Redis (broker) → Celery Worker → Result backend
| |
└──────────── .delay() / .apply_async() ─────────────┘
Redis is the most common broker for small-to-medium deployments because you likely already use it for caching. RabbitMQ offers stronger delivery guarantees for high-reliability systems.
Getting Started
Install dependencies:
pip install celery redis
Create a Celery application:
# myapp/celery_app.py
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,
task_track_started=True,
task_acks_late=True, # requeue if worker crashes mid-task
)
Define a task:
# myapp/tasks.py
from myapp.celery_app import app
import time
@app.task(bind=True, max_retries=3, default_retry_delay=60)
def send_welcome_email(self, user_id: int):
try:
user = get_user(user_id)
mailer.send(user.email, "Welcome!", body=render_template(user))
except SMTPException as exc:
raise self.retry(exc=exc)
Call it from your web app:
# In a Flask/FastAPI route
from myapp.tasks import send_welcome_email
def register_user(data):
user = create_user(data)
send_welcome_email.delay(user.id) # returns immediately
return {"user_id": user.id}
Start a worker:
celery -A myapp.celery_app worker --loglevel=info
Task Design Best Practices
Keep Tasks Idempotent
Workers may retry tasks after partial execution. Design tasks so running them twice does not cause duplicate charges, emails, or database records. Use unique constraints and check-before-act patterns.
Pass Serializable Arguments
Celery serializes task arguments. Pass IDs and primitives, not ORM objects or open file handles.
# Good
process_invoice.delay(invoice_id=42)
# Bad — Django model instances do not serialize reliably
process_invoice.delay(invoice=invoice_obj)
Set Time Limits
Prevent runaway tasks from blocking workers forever:
app.conf.task_soft_time_limit = 300 # 5 min soft warning
app.conf.task_time_limit = 360 # 6 min hard kill
Use Task Routing for Priority
Route critical tasks to dedicated queues:
app.conf.task_routes = {
"myapp.tasks.send_password_reset": {"queue": "high_priority"},
"myapp.tasks.generate_monthly_report": {"queue": "low_priority"},
}
Run specialized workers:
celery -A myapp.celery_app worker -Q high_priority --concurrency=4
celery -A myapp.celery_app worker -Q low_priority --concurrency=2
Scheduled Tasks with Celery Beat
Celery Beat is a scheduler that sends tasks at defined intervals or cron schedules.
from celery.schedules import crontab
app.conf.beat_schedule = {
"cleanup-expired-sessions": {
"task": "myapp.tasks.cleanup_sessions",
"schedule": crontab(hour=3, minute=0), # daily at 3 AM UTC
},
"sync-inventory-every-15-min": {
"task": "myapp.tasks.sync_inventory",
"schedule": 900.0, # seconds
},
}
Run the beat process alongside workers:
celery -A myapp.celery_app beat --loglevel=info
In production, run only one beat instance to avoid duplicate scheduling.
Monitoring and Debugging
Flower is a web-based monitoring tool for Celery:
pip install flower
celery -A myapp.celery_app flower --port=5555
It shows active tasks, worker status, and task history. Useful in staging; protect it with authentication in production.
Common debugging steps:
- Check worker logs for tracebacks.
- Verify Redis connectivity from both web app and worker containers.
- Confirm task names match between producer and worker (import paths matter).
- Inspect the broker queue length:
redis-cli LLEN celery. - Test tasks synchronously in a shell:
send_welcome_email.apply(args=[1]).
Production Deployment Notes
- Run workers as systemd services, Docker containers, or Kubernetes deployments — not inside web server processes.
- Scale workers independently from web servers based on queue depth.
- Use
task_acks_late=Trueso tasks return to the queue if a worker dies mid-execution. - Set
worker_prefetch_multiplier=1for long-running tasks to prevent one worker from hoarding jobs. - Monitor queue depth and task failure rates as core application metrics.
- Plan broker persistence — if Redis restarts without AOF/RDB, queued tasks can be lost.
Celery vs Alternatives
| Tool | Best for |
|---|---|
| Celery | General-purpose Python task queues, mature ecosystem |
| RQ (Redis Queue) | Simpler setup, smaller projects |
| Dramatiq | Modern API, good defaults, less Django-centric |
| AWS SQS + Lambda | Serverless, no worker management |
| Huey | Lightweight, Redis-backed, minimal config |
Celery has the largest community and most third-party integrations, which is why it remains the default choice despite a steeper learning curve.
FAQ
Do I need a separate Redis instance for the broker and result backend?
Not required, but using different Redis databases (e.g., /0 for broker, /1 for results) keeps data separated and simplifies debugging.
What happens if all workers are busy? Tasks wait in the broker queue. Monitor queue depth and scale workers if wait times grow consistently.
Can I run Celery with Django?
Yes. Django has official Celery integration docs. Place celery.py in your project root and autodiscover tasks from installed apps.
How do I test Celery tasks?
Use task_always_eager=True in test settings to execute tasks synchronously in-process without a broker.
Is Celery overkill for a side project? Often yes. Start with a cron job or inline async processing. Adopt Celery when reliability, retries, and scaling actually matter.
Celery gives Python applications a reliable way to handle work that does not belong in a request cycle. Start with a single worker and one queue, design tasks to be idempotent, and add routing and monitoring as your traffic grows.
Handling Task Failures Gracefully
Not every task succeeds on the first attempt. Network blips, rate limits, and transient database locks are normal. Celery's retry mechanism helps, but you need a dead-letter strategy for tasks that fail permanently.
Configure autoretry for transient errors and set a reasonable max_retries value. After max retries, log the failure to your monitoring system and alert on-call if the task is business-critical. Store failed task metadata (arguments, traceback, timestamp) in a database table for manual replay.
For non-critical tasks, a simple admin dashboard that lists failed jobs and lets operators retry them manually is often enough.
Integrating Celery with FastAPI
FastAPI does not include Celery by default, but integration is straightforward. In your route handler, call task.delay() with the resource identifier and return immediately with a processing status. The client can poll an endpoint or receive a webhook when the task completes.
Use FastAPI's BackgroundTasks only for trivial work that completes in under a second. Anything longer belongs in Celery.
Performance Tuning Checklist
- Pool type — use prefork (default) for CPU-bound tasks, gevent or eventlet for I/O-bound tasks with many concurrent connections.
- Concurrency — start with concurrency equal to the number of CPU cores for CPU-bound work. Monitor and adjust.
- Result expiry — set result_expires so the result backend does not grow unbounded.
- Compression — enable task compression for large task payloads.
- Connection pooling — reuse database connections in workers with pool settings appropriate for long-lived processes.
Celery rewards teams that invest in observability early. When a background job silently fails at 2 AM, good logging and alerting are what separate a minor incident from a customer-facing outage.
Comments
Loading comments…