Deploying and Operating Celery
Deployment patterns, worker pools, prefetch fairness, scaling, monitoring, and security settings for running Celery in production
Contents
Running Celery in production means tuning broker reliability, worker availability, monitoring, and performance together.
Why deployment is the hard part
Celery is a distributed task queue: tasks are pushed onto a message queue, an intermediate store for passing work asynchronously, and workers pull them off and run them. A single-host demo starts with one command; production does not. If the broker stops, queued tasks disappear, and if workers are overloaded, responses back up.
Deployment therefore has to address four axes at once: message delivery reliability, worker availability, monitoring, and performance tuning. The default values quoted below come from the official Celery documentation (docs.celeryq.dev).
Three deployment patterns
Pick one of the following based on whether you are deploying to a single host or many. Scale and load variability decide the answer.
| Pattern | Fits | Scaling | Notes |
|---|---|---|---|
| Docker Compose | Single host, small scale | Manual (adjust replicas) | Fastest to set up |
| Kubernetes | Multiple hosts, variable load | Automatic via HPA | Probes and rolling updates |
| systemd | Directly on VMs | Manual | OS integration, no external dependencies |
Docker Compose bundles the broker, result backend, workers, and scheduler into a single file. A production configuration adds health checks, restart policies, and volume persistence.
services:
rabbitmq:
image: rabbitmq:3.12-management-alpine
environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS}
RABBITMQ_DEFAULT_VHOST: /celery
healthcheck:
test: rabbitmq-diagnostics -q ping
interval: 10s
timeout: 5s
retries: 5
worker:
build: .
command: celery -A myapp worker -l INFO -c 4 --max-tasks-per-child 1000 --max-memory-per-child 200000
depends_on:
rabbitmq:
condition: service_healthy
environment:
CELERY_BROKER_URL: amqp://${RABBITMQ_USER}:${RABBITMQ_PASS}@rabbitmq//celery
CELERY_RESULT_BACKEND: redis://:${REDIS_PASS}@redis:6379/1
restart: unless-stoppedOn Kubernetes, workers run as a Deployment with health probes and a termination grace period. The liveness probe uses inspect ping to check that the worker is alive. The readiness probe uses inspect active to check whether it is ready to accept work.
apiVersion: apps/v1
kind: Deployment
metadata:
name: celery-worker
spec:
replicas: 3
template:
spec:
terminationGracePeriodSeconds: 120
containers:
- name: worker
image: myregistry/myapp:latest
command: ["celery", "-A", "myapp", "worker"]
args: ["-l", "INFO", "-c", "4", "--max-tasks-per-child", "1000"]
env:
- name: CELERY_BROKER_URL
valueFrom:
secretKeyRef:
name: celery-secrets
key: broker-url
resources:
requests: { memory: "256Mi", cpu: "500m" }
limits: { memory: "512Mi", cpu: "1000m" }
livenessProbe:
exec:
command: ["celery", "-A", "myapp", "inspect", "ping"]
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command: ["celery", "-A", "myapp", "inspect", "active"]
initialDelaySeconds: 10
periodSeconds: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0On SIGTERM a worker performs a warm shutdown: it finishes the tasks already in flight before stopping. That is why terminationGracePeriodSeconds has to exceed the runtime of your longest task. Set it too low and Kubernetes sends SIGKILL, cutting tasks off midway.
When running directly on a VM, register the worker as a systemd unit. Type=notify lets the worker signal readiness to systemd, and a restart policy brings it back after failure.
# /etc/systemd/system/celery-worker.service
[Service]
Type=notify
User=celery
Group=celery
WorkingDirectory=/opt/myapp
Environment="CELERY_BROKER_URL=redis://localhost:6379/0"
ExecStart=/usr/local/bin/celery -A myapp worker \
-l INFO -c 4 \
--max-tasks-per-child=1000 \
--max-memory-per-child=200000
Restart=on-failure
RestartSec=10Worker pools and concurrency
The pool type determines how a worker runs tasks in parallel. The right choice depends on whether tasks burn CPU or wait on I/O.
| Pool | CPU-bound | I/O-bound | Concurrency | Memory |
|---|---|---|---|---|
| prefork | Best | Good | Low to medium | High |
| eventlet | Poor fit | Best | Very high | Low |
| gevent | Poor fit | Best | Very high | Low |
| solo | Sequential | Sequential | 1 | Low |
The default, prefork, spawns a process per task and uses every CPU core. eventlet and gevent use green threads, lightweight threads that switch cooperatively in user space. A single process can hold thousands of concurrent I/O waits. Use the following as a selection rule.
I/O-bound + many concurrent connections → eventlet / gevent
CPU-bound or memory leak risk → prefork (default)
Development / testing → soloConcurrency is set with -c. When load varies, --autoscale gives an upper and lower bound so the worker grows and shrinks its process count on its own.
# CPU work: one prefork process per core
celery -A myapp worker -l INFO -c 4
# I/O work: concurrency 1000 with gevent
celery -A myapp worker -P gevent -c 1000
# Variable load: between 4 and 16 processes
celery -A myapp worker -l INFO --autoscale=16,4eventlet and gevent do not support max_tasks_per_child. If you suspect a memory leak under those pools, you need separate monitoring and periodic restarts.
Prefetch and fairness
Workers pull tasks ahead of time for throughput. The number pulled at once is concurrency x worker_prefetch_multiplier, and the default multiplier is 4 (per the official Celery documentation).
Prefetching is faster when tasks are short and numerous. Long tasks are the problem. A long task at the head of the batch leaves everything pulled with it waiting behind it.
| Task type | Recommended multiplier | Reason |
|---|---|---|
| Short and numerous | 4 (default) | Prefetching raises throughput |
| Long and heavy | 1 | One at a time keeps things fair |
Do not mix long and short tasks on the same worker. Splitting queues and dedicating workers per queue keeps congestion on one side from blocking the other.
# Short task queue: high concurrency
celery -A myapp worker -Q short -l INFO -c 8
# Long task queue: low concurrency, prefetch 1, with timeouts
celery -A myapp worker -Q long -l INFO -c 2 \
--time-limit=3600 --soft-time-limit=3500The long-task worker should also set app.conf.worker_prefetch_multiplier = 1. Queue separation and prefetch 1 together are what stop a long task from trapping others.
Scaling
Scaling goes in three directions: horizontally by adding workers, vertically by raising concurrency, and by partitioning queues. Adding workers that consume the same queue increases throughput.
On Kubernetes, the Horizontal Pod Autoscaler (HPA) adjusts worker pods to match load. It moves between the minimum and maximum replica counts based on CPU and memory utilization.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: celery-worker-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: celery-worker
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70CPU utilization alone sometimes misses a queue that is backing up. If pending message count is what matters for your workload, exposing queue depth as a separate metric and scaling on it is more accurate.
Monitoring
Flower is the standard web monitoring tool for Celery. It shows worker status, task execution history, and live statistics, and it also supports remote control such as revoking tasks or adjusting rate limits.
# Start Flower
celery -A myapp flower --port=5555Monitoring works by consuming the events workers emit. Event sending has to be enabled before Flower can trace task flow.
app.conf.worker_send_task_events = True
app.conf.task_send_sent_event = True
app.conf.task_track_started = TrueFor long-term metrics, Prometheus scrapes Flower's /metrics endpoint. Control commands go through the inspect and control application programming interface (API). The diagram below traces the path from task publication to monitoring.
Two things stand out in the diagram. The task path (producer to broker to worker to result) and the observation path (worker to events to monitoring) are separate. Task processing therefore continues even when monitoring is down.
Reliability and security settings
If a worker acknowledges a task the moment it receives it, a crash mid-execution loses the task. task_acks_late=True defers the acknowledgement until the task finishes, preventing that loss. The trade-off is that the same task may run twice, so tasks must be idempotent, meaning repeated processing of the same input yields the same result.
app.conf.task_acks_late = True
app.conf.task_reject_on_worker_lost = True
app.conf.worker_prefetch_multiplier = 1Long-running prefork workers can creep upward in memory usage. Recycling the child process after a task count or memory threshold cuts the leak off.
app.conf.worker_max_tasks_per_child = 1000 # recycle after 1000 tasks
app.conf.worker_max_memory_per_child = 200_000 # recycle above 200MB (value in KB)Security starts with serialization. Pickle serialization allows arbitrary code execution, so use JSON. Workers run as a dedicated user, never as root.
app.conf.task_serializer = 'json'
app.conf.accept_content = ['json']# Run as a dedicated user
sudo -u celery celery -A myapp workerIf you also need to prevent message tampering, enable cryptographic signing. Messages are signed with a key and certificate, and the serialization format changes to auth. The broker connection itself is encrypted with Transport Layer Security (TLS).
from celery.security import setup_security
setup_security(
app,
key='/path/to/key.pem',
cert='/path/to/cert.pem',
store='/path/to/ca.pem',
digest='sha256',
)
app.conf.task_serializer = 'auth'
app.conf.accept_content = ['auth']
app.conf.broker_use_ssl = TrueSummary
Production Celery does not end with picking a deployment pattern. Reliability and observability have to be tuned alongside it before the system runs stably. Choose Docker Compose, Kubernetes, or systemd according to the environment. Give workers a pool and concurrency that match the shape of the work, and split long tasks into their own queue with prefetch set to 1 so they do not trap short ones.
Monitoring collects events through Flower and Prometheus. Reliability comes from acks_late plus idempotency; security comes from JSON serialization, a dedicated user, and message signing. Before deploying, it pays to walk a checklist once: broker high availability, result backend, monitoring, alerting, security audit, and a performance baseline.