import ComparisonTable from ’../../components/ComparisonTable.astro’;
Background task processing is essential for any Python web application — sending emails, processing uploads, generating reports, or running scheduled jobs. Celery has dominated this space for over a decade; Dramatiq is the modern challenger focused on reliability and simplicity.
Quick Verdict
Choose Celery if: You need Django integration, complex task routing, scheduled tasks (Celery Beat), or your team has existing Celery expertise.
Choose Dramatiq if: You want simpler code, better message reliability guarantees, or are starting fresh without legacy constraints.
Feature Comparison
<ComparisonTable headers={[“Feature”, “Celery”, “Dramatiq”]} rows={[ [“API design”, “Decorator-based”, “Decorator-based (simpler)”], [“Brokers”, “Redis, RabbitMQ, SQS, more”, “Redis, RabbitMQ”], [“Result backends”, “Redis, DB, Memcached, S3”, “None built-in (use Redis directly)”], [“Message reliability”, “At-least-once (configurable)”, “At-least-once (better defaults)”], [“Retry logic”, “Manual (retry() method)”, “Automatic (middleware)”], [“Scheduled tasks”, “Celery Beat (separate)”, “APScheduler or cron (external)”], [“Django integration”, “django-celery-beat (excellent)”, “Works but less native”], [“Monitoring”, “Flower (web UI)”, “Dramatiq-dashboard (basic)”], [“Documentation”, “Extensive”, “Good but thinner”], [“Stars (GitHub)”, “23K+”, “4K+”], ]} />
Basic Task Definition
Celery:
# tasks.py
from celery import Celery
from celery.utils.log import get_task_logger
app = Celery('myapp', broker='redis://localhost:6379/0')
app.config_from_object('django.conf:settings', namespace='CELERY')
logger = get_task_logger(__name__)
@app.task(bind=True, max_retries=3, default_retry_delay=60)
def send_welcome_email(self, user_id: int) -> None:
try:
user = User.objects.get(id=user_id)
send_email(user.email, "Welcome!", render_template("welcome.html", user=user))
logger.info(f"Welcome email sent to {user.email}")
except User.DoesNotExist:
logger.error(f"User {user_id} not found")
raise
except EmailServiceError as exc:
logger.warning(f"Email failed, retrying in 60s")
raise self.retry(exc=exc)
# Call the task
send_welcome_email.delay(user_id=42)
# Call with options
send_welcome_email.apply_async(
args=[42],
countdown=30, # delay 30 seconds
queue='high-priority'
)
Dramatiq:
# tasks.py
import dramatiq
from dramatiq.brokers.redis import RedisBroker
from dramatiq.middleware import Retries, TimeLimit, Callbacks
dramatiq.set_broker(RedisBroker(host="localhost"))
@dramatiq.actor(
queue_name="email",
max_retries=3,
min_backoff=60_000, # 60 seconds in ms
max_backoff=3_600_000, # 1 hour
)
def send_welcome_email(user_id: int) -> None:
user = User.objects.get(id=user_id)
send_email(user.email, "Welcome!", render_template("welcome.html", user=user))
# Call the task
send_welcome_email.send(42)
# Call with options
send_welcome_email.send_with_options(
args=(42,),
delay=30_000, # 30 second delay in ms
)
Dramatiq’s API is slightly simpler — no self parameter needed, backoff configured declaratively.
Celery Configuration (Django)
# settings.py
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'
# Reliability settings
CELERY_TASK_ACKS_LATE = True # Don't ack until task completes
CELERY_TASK_REJECT_ON_WORKER_LOST = True # Requeue if worker dies
CELERY_TASK_TRACK_STARTED = True
# Queue routing
CELERY_TASK_ROUTES = {
'myapp.tasks.send_*': {'queue': 'email'},
'myapp.tasks.process_*': {'queue': 'processing'},
'myapp.tasks.report_*': {'queue': 'reports'},
}
CELERY_TASK_QUEUES = {
'email': {'exchange': 'email', 'routing_key': 'email'},
'processing': {'exchange': 'processing', 'routing_key': 'processing'},
'reports': {'exchange': 'reports', 'routing_key': 'reports'},
}
# celery.py
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()
Dramatiq Configuration
# dramatiq_setup.py
import dramatiq
from dramatiq.brokers.redis import RedisBroker
from dramatiq.middleware import (
AgeLimit,
Callbacks,
Pipelines,
Retries,
ShutdownNotifications,
TimeLimit,
)
broker = RedisBroker(
host="localhost",
port=6379,
db=0,
middleware=[
AgeLimit(), # Drop messages older than max_age
TimeLimit(), # Kill tasks running too long
ShutdownNotifications(), # Graceful shutdown
Callbacks(), # on_success/on_failure callbacks
Pipelines(), # Chain tasks
Retries(min_backoff=1000, max_backoff=3_600_000, max_retries=5),
]
)
dramatiq.set_broker(broker)
Dramatiq’s middleware system is cleaner — each feature is an explicit middleware.
Scheduled Tasks
Celery Beat:
# settings.py
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
'daily-report': {
'task': 'myapp.tasks.generate_daily_report',
'schedule': crontab(hour=8, minute=0),
'args': (),
},
'hourly-sync': {
'task': 'myapp.tasks.sync_external_data',
'schedule': crontab(minute=0),
},
'every-5-minutes': {
'task': 'myapp.tasks.check_alerts',
'schedule': crontab(minute='*/5'),
},
}
Run Celery Beat separately: celery -A myapp beat -l info
Dramatiq — external scheduling:
# Using APScheduler with Dramatiq
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
scheduler = BackgroundScheduler()
scheduler.add_job(
lambda: generate_daily_report.send(),
CronTrigger(hour=8, minute=0),
id='daily-report',
)
scheduler.start()
Dramatiq requires an external scheduler — APScheduler, Rocketry, or a system cron.
Error Handling and Reliability
Celery reliability patterns:
@app.task(
bind=True,
max_retries=5,
default_retry_delay=30,
autoretry_for=(TemporaryError,), # Auto-retry for specific exceptions
retry_backoff=True, # Exponential backoff
retry_backoff_max=600, # Max 10 minutes between retries
retry_jitter=True, # Random jitter to avoid thundering herd
)
def reliable_task(self, data: dict) -> None:
try:
process(data)
except PermanentError:
# Don't retry permanent failures
self.update_state(state='FAILURE', meta={'error': 'permanent'})
raise
except TemporaryError as exc:
raise self.retry(exc=exc)
# Task signals for monitoring
from celery.signals import task_failure, task_retry
@task_failure.connect
def handle_task_failure(task_id, exception, **kwargs):
alert_oncall(f"Task {task_id} failed: {exception}")
@task_retry.connect
def handle_task_retry(request, reason, **kwargs):
log_retry(request.task, reason)
Dramatiq reliability:
@dramatiq.actor(
max_retries=5,
min_backoff=30_000,
max_backoff=600_000,
throws=(PermanentError,), # Don't retry these
)
def reliable_task(data: dict) -> None:
process(data) # Dramatiq handles retries automatically on exception
# Middleware-level error tracking
class ErrorTrackingMiddleware(dramatiq.Middleware):
def after_process_message(self, broker, message, *, result=None, exception=None):
if exception is not None:
sentry_sdk.capture_exception(exception)
Dramatiq’s automatic retry behavior with cleaner defaults is one of its key advantages.
Monitoring
Celery Flower:
# Install and run Flower
pip install flower
celery -A myapp flower --port=5555
# Access at http://localhost:5555
# Shows: Active tasks, queues, workers, task history, rate limits
Prometheus metrics (both):
# Celery + prometheus-celery-exporter
# Dramatiq + dramatiq-prometheus
# Both expose worker metrics for Grafana dashboards
Production Deployment
Celery workers:
# docker-compose.yml
services:
celery-default:
command: celery -A myapp worker -Q default -c 4
celery-email:
command: celery -A myapp worker -Q email -c 2
celery-beat:
command: celery -A myapp beat
flower:
command: celery -A myapp flower
ports: ["5555:5555"]
Dramatiq workers:
services:
dramatiq-default:
command: python -m dramatiq myapp.tasks --processes 4 --threads 8 --queues default
dramatiq-email:
command: python -m dramatiq myapp.tasks --processes 2 --threads 4 --queues email
Bottom Line
Celery is the production-proven choice with a massive ecosystem, Django-native integration, and extensive documentation. If you’re starting a new project and want simpler, more reliable defaults, Dramatiq is worth serious consideration. For most Django applications, Celery with careful configuration is the pragmatic choice. For greenfield Python services where Django’s ecosystem isn’t a factor, Dramatiq’s cleaner API is genuinely appealing.