Home DevOps Airflow Celery: Dead Workers Nobody Noticed? Fix It
Advanced 3 min · August 1, 2026
Airflow Celery Setup

Airflow Celery: Dead Workers Nobody Noticed? Fix It

Airflow Celery workers die silently and nobody notices.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Production
production tested
August 01, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 15-20 min read
  • Airflow running with a working DAG.
  • Understanding of executors and parallelism (see the executors article).
  • Docker or a VM where Redis or RabbitMQ can run.
  • Basic familiarity with systemd or Kubernetes for supervision.
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • CeleryExecutor ships task instances from the scheduler to a fleet of Celery workers over a broker: Redis or RabbitMQ.
  • Workers are stateless and must be supervised: Celery never restarts a dead worker, so systemd or Kubernetes must.
  • worker_autoscale grows concurrency with queue depth, but caps it by memory: 12,3 means 12 max, 3 idle.
  • The broker visibility timeout re-delivers unacked tasks: set it above your longest task or tasks run twice.
  • Flower gives fleet visibility: worker heartbeats, queue depth, and task state in one tab.
  • Dead workers show up as a silent backlog: queued tasks age while the grid view stays green.
✦ Definition~90s read
What is Airflow Celery Setup?

Airflow Celery setup is the distributed executor pattern: a broker carries task instances from the scheduler to a supervised, autoscaled fleet of Celery workers that report results to the metadata DB.

Celery workers are like a team of couriers picking up packages from a dispatch office.
Plain-English First

Celery workers are like a team of couriers picking up packages from a dispatch office. The broker is the office where tasks wait. Couriers die quietly, and nobody at the office notices or hires replacements: the packages just pile up. Flower is the security camera showing which couriers are still alive. The visibility timeout is the rule that a package gets handed to a new courier if the first one never confirmed it.

CeleryExecutor is how Airflow grows from one machine to a fleet. The scheduler stays put; the work travels over a broker to workers that can live anywhere.

Workers are stateless and replaceable, which makes them great. It also makes them disposable: when a worker OOMs at 3 AM, nothing brings it back. Celery doesn't restart workers. It doesn't page anyone. It just lets the queue grow.

That's the whole story of this article: the difference between a platform that looks healthy and one that is healthy. Flower, heartbeats, queue depth, and a visibility timeout are the difference.

Treat your workers as cattle, and build the fences yourself.

1. Why Celery: Decoupled Task Execution

LocalExecutor runs tasks on the scheduler host. That couples scheduling to execution: the moment your nightly batch needs more cores, you're buying a bigger scheduler.

Celery decouples them. The scheduler hands task instances to a broker, and any number of workers pull from the broker wherever they live. Capacity becomes a fleet problem, not a host problem.

Workers report results back to the metadata DB through a result backend. The scheduler never talks to workers directly: broker in, result backend out, contract in between.

That contract is why workers are disposable. Any worker can run any task. The fleet just needs to exist.

One consequence worth internalizing: with CeleryExecutor the scheduler never runs tasks itself — it only queues them. Any node that must also execute work needs its own worker process running there.

📊 Production Insight
Broker in, result backend out, scheduler never talks to workers.
Capacity is a fleet problem, not a host problem.
Rule: stateless workers are the point; supervise them anyway.
🎯 Key Takeaway
Celery decouples scheduling from execution via a broker.
Workers are disposable by design.
Punchline: the broker is the contract, the workers are the capacity.
Celery: Decoupled Task Execution Celery: Decoupled Task Execution Broker in, result backend out Scheduler Hands task instances to the broker Broker (Redis) Tasks wait here for a worker Worker fleet Any worker can run any task Result backend Results to the metadata DB Capacity = fleet problem Not a bigger scheduler host THECODEFORGE.IO
thecodeforge.io
Airflow Celery Setup

2. Broker Setup: Redis vs RabbitMQ

The broker is the queue where task instances wait for a worker. Two brokers dominate: Redis and RabbitMQ.

Redis is the pragmatic pick: fast, already in your stack, one container away. Its queue mechanics are simple, and most Airflow installs never need more. RabbitMQ is the heavyweight: real queue semantics, per-queue persistence, and finer control over delivery.

The visibility-timeout behavior differs. On Redis, unacked tasks are re-delivered after a configurable visibility timeout; on RabbitMQ, the ack timeout governs redelivery. Both can double-execute a task if misconfigured.

Pick Redis for speed and simplicity, RabbitMQ when you need robust queueing semantics. Either works; neither forgives a missing timeout.

One config note: Airflow's visibility_timeout is only honored for Redis and SQS brokers; RabbitMQ has no such knob — its redelivery is governed by Celery's ack semantics instead.

broker.shBASH
1
2
3
4
5
6
7
8
9
# Redis broker: one container, zero config
docker run -d --name airflow-redis -p 6379:6379 redis:7-alpine

# RabbitMQ broker: heavier, richer queue semantics
docker run -d --name airflow-rabbitmq -p 5672:5672 -p 15672:15672 \
  rabbitmq:3-management

# Point Airflow at the broker
airflow config get-value celery broker_url
Output
redis://redis:6379/0
🔥Redis for Speed, RabbitMQ for Semantics
Redis handles most Airflow fleets comfortably and is easier to operate. RabbitMQ earns its complexity when you need per-queue persistence and precise delivery guarantees.
📊 Production Insight
Redis: fast, simple, one container. RabbitMQ: real queue semantics.
Both re-deliver unacked tasks; both can double-run.
Rule: match broker to fleet size, never to fashion.
🎯 Key Takeaway
Redis and RabbitMQ both work as Airflow brokers.
Misconfigured redelivery double-executes tasks on either.
Punchline: the broker choice matters less than its timeouts.

3. Workers, Queues, and Autoscale

A worker runs with flags that define its identity: -H gives it a name, --queues lists what it listens on, and --autoscale sets its concurrency window. The window is the memory contract: 12,3 means up to 12 concurrent tasks when the queue is deep, settling to 3 when it's quiet.

Queues route work. A worker started with --queues=default,heavy_ml only takes tasks queued to those names. Fast batch and long GPU jobs can share a fleet without starving each other.

Autoscale is elasticity, not survival. It grows concurrency within one worker process; it cannot resurrect a dead host. A fleet of zero workers autoscales to zero.

Size the concurrency window to memory. Autoscale above your RAM and you're scheduling OOM kills.

start_worker.shBASH
1
2
3
4
5
6
7
8
# Worker on host prod-1, listening on default and heavy_ml queues
# Autoscale: up to 12 concurrent tasks, settling to 3 when idle
airflow celery worker -H worker@prod-1 \
  --queues=default,heavy_ml \
  --autoscale=12,3 --loglevel=INFO

# Watch it start
airflow celery worker --help | grep autoscale
Output
Worker worker@prod-1 started. Queues: default, heavy_ml
📊 Production Insight
Autoscale window = memory contract: 12,3 caps concurrency.
Queues route work; a listener must exist for every queue.
Rule: size the window to RAM, not to hope.
🎯 Key Takeaway
Worker identity is name, queues, and autoscale window.
Autoscale is elasticity, not a resurrection service.
Punchline: a dead host autoscales to zero workers.

4. Flower: Fleet Visibility in One Tab

Flower is the Celery web UI. One command, one port, and you can see every worker, its heartbeat, the tasks it's running, and the queues in front of it. Start it with airflow celery flower --broker=redis://redis:6379/0 and open port 5555.

This is the screen that would have saved the night: worker count, heartbeat freshness, and queue depth at a glance. Zero workers with stale heartbeats is the failure signature of a dead fleet.

Flower alone isn't alerting, though. A dashboard nobody watches is decoration. Export the numbers: worker count and queue depth to Prometheus, alerts to Slack or PagerDuty.

Every fleet needs both: the tab to look at, and the alert that looks for you.

One dependency to know: Flower relies on Celery's remote control (worker_enable_remote_control). Disabling that setting to suppress the .*reply-celery-pidbox reply queues Celery creates on unsupported brokers also breaks Flower — the two are tied together.

📊 Production Insight
Flower shows workers, heartbeats, and queue depth.
A dashboard nobody watches is decoration.
Rule: export queue depth and worker count into alerting.
🎯 Key Takeaway
Flower is the fleet's status screen.
Visibility without alerting is just a nicer way to be late.
Punchline: watch it, and let it page you.
Flower: Fleet Visibility in One Tab Flower: Fleet Visibility in One Tab Heartbeats, queues, tasks — one tab Flower web UI airflow celery flower · port 5555 Worker heartbeats Stale heartbeat = dead fleet signature Tasks + queue depth Backlog in front of each worker Visibility without alerting An unwatched dashboard is decoration Export to alerting Queue depth + heartbeats → pages THECODEFORGE.IO
thecodeforge.io
Airflow Celery Setup

5. Worker Death: Detect, Restart, Drain

Celery's failure mode is silence. A worker dies, tasks stay queued, and nothing happens. The fix is supervision: a process manager that treats workers as disposable.

systemd is the classic answer on VMs: Restart=always, RestartSec=10, and a worker that dies gets reborn within seconds. Kubernetes plays the same role for containerized fleets.

Draining matters when you take a worker down on purpose. Stop accepting new tasks, let the running ones finish, then stop the process. A kill while tasks run mid-flight is how double execution starts.

The pattern is always the same: detect via heartbeat, restart via supervisor, drain before maintenance.

airflow-worker.serviceINI
1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=Airflow Celery Worker
After=network-online.target

[Service]
User=airflow
ExecStart=/opt/airflow/airflow celery worker \
  -H worker@prod-1 --queues=default,heavy_ml --autoscale=12,3
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
Output
systemd restarts a dead worker within 10 seconds
📊 Production Insight
Supervision is mandatory: Restart=always or Kubernetes.
Drain workers before maintenance to avoid mid-flight kills.
Rule: detect with heartbeats, restart with a supervisor, drain before touching.
🎯 Key Takeaway
Celery never resurrects a dead worker; a supervisor does.
Draining finishes in-flight tasks before a stop.
Punchline: treat workers as cattle and supervise the herd.

6. The Visibility-Timeout Double-Execution Trap

Here's the trap that makes task logs lie. On Redis, a task is a message. When a worker fetches it, the message enters an unacked state with a window: the visibility timeout.

If the worker dies or takes longer than the window, Redis considers the task lost and re-delivers it. Another worker runs it. Your task executed twice, and both runs wrote "success."

Airflow's default visibility timeout for Redis and SQS brokers is 86400 seconds — 24 hours — so the classic failure is a task that runs longer than a day: the broker redelivers it while the original worker keeps going, and both complete. The UI and logs show the tell: 'Task Instance Not Running' FAILED: Task is in the running state. Set visibility_timeout above your longest realistic task runtime and re-check it whenever a new long task ships. Airflow also sets task_acks_late=True by default, but on Redis and SQS that does not override the visibility timeout — the broker still redelivers once the window expires.

This is also why tasks must be idempotent: even with correct timeouts, crashes and redelivery happen. Idempotency is the last line of defense, and it should be the first.

airflow.cfgINI
1
2
3
4
5
6
7
8
9
[celery]
broker_url = redis://redis:6379/0
result_backend = db+postgresql://airflow:airflow@postgres:5432/airflow
worker_autoscale = 12,3
worker_prefetch_multiplier = 1

[celery_broker_transport_options]
# Above the longest task runtime (6h ETL = 21600s)
visibility_timeout = 21600
Output
visibility_timeout = 21600
⚠ The 24-Hour Default
Airflow defaults to a 24-hour visibility timeout on Redis and SQS. Any task running longer gets redelivered mid-execution. Set it above your longest task, then re-check after every new long task ships.
📊 Production Insight
Unacked tasks redeliver after the visibility window.
A task past the window redelivers mid-run; the default is 24h.
Rule: timeout above your longest task, idempotency below everything.
🎯 Key Takeaway
Visibility timeout governs when the broker redelivers.
Too short a window means double execution with success logs.
Punchline: idempotent tasks survive the redeliveries you haven't hit yet.
The Visibility-Timeout Double-Run Trap The Visibility-Timeout Double-Run Trap Why task logs lie on Redis Task = message on Redis Fetched by a worker → unacked Visibility timeout window Default: ~30 seconds Acknowledged in time Runs exactly once Worker dies or runs long Broker re-delivers the task Double execution Both runs log success — logs lie Timeout above longest task Idempotency as the backstop THECODEFORGE.IO
thecodeforge.io
Airflow Celery Setup
● Production incidentPOST-MORTEMseverity: high

The Night Every Worker Vanished

Symptom
At 3 AM, task instances entered queued and stayed there for six hours. The UI showed a growing backlog while no worker existed to pick anything up. Every DAG run that night finished late or failed.
Assumption
The team assumed Celery restarted workers automatically, the way a service manager restarts a crashed process.
Root cause
Workers were started once, statically, with no supervisor. An OOM kill on the worker host took every worker down for good. Nothing respawned them, and no alert existed on worker count or queue depth, so the fleet stayed dead until morning.
Fix
Wrapped workers in systemd with Restart=always, switched to worker_autoscale capped by memory, added Flower-based visibility, and alerted on queue depth and worker heartbeat so a dead fleet paged someone immediately.
Key lesson
  • Celery does not restart dead workers. That's your job.
  • Static workers die quietly; a supervisor is not optional.
  • Queue depth and worker count need alerts, not eyeballs.
  • Set a broker visibility timeout, or re-delivered tasks run twice.
Production debug guideSymptom to Action5 entries
Symptom · 01
Tasks queued for minutes with no progress
Fix
Open Flower and check worker heartbeats. Zero workers means the fleet is dead; restart it and page the on-call.
Symptom · 02
Worker keeps OOM-killing
Fix
Compare worker concurrency against host memory. Reduce --autoscale max or add memory; a concurrency of 12 needs more than 4 GB of tasks.
Symptom · 03
Tasks run twice after a worker dies
Fix
The broker re-delivered unacked tasks. Raise visibility_timeout above your longest task, or tasks get redelivered mid-execution.
Symptom · 04
Some queues never drain
Fix
Compare task queue attributes to worker --queues lists. A task queued to heavy_ml waits forever if no worker listens there.
Symptom · 05
Worker heartbeats flap on and off
Fix
Check network and broker health. Flapping heartbeats usually mean broker connection issues or a host under memory pressure.
★ Quick Debug Cheat SheetCommon Celery issues and immediate actions
Zero workers, tasks stuck queued
Immediate action
Check fleet health
Commands
airflow celery flower
airflow celery worker -H worker@prod-1 --autoscale=12,3
Fix now
Restart workers under systemd; alert on worker count immediately.
Worker OOM-killed at night+
Immediate action
Right-size concurrency
Commands
airflow config get-value celery worker_autoscale
dmesg | grep -i oom | tail -20
Fix now
Lower autoscale max or raise host memory; add a supervisor.
Tasks executing twice+
Immediate action
Check the visibility window
Commands
airflow config get-value celery_broker_transport_options visibility_timeout
airflow config get-value celery broker_url
Fix now
Set visibility_timeout above your longest task runtime.
Backlog growing, no alerts fired+
Immediate action
Add queue-depth alerting
Commands
redis-cli -h redis llen airflow.executor.celery
airflow config get-value celery result_backend
Fix now
Alert on queue length and worker heartbeat in your monitoring stack.
Redis vs RabbitMQ as the Airflow Broker
AspectRedisRabbitMQ
Delivery semanticsPub/sub plus lists; unacked tasks redeliver after visibility_timeoutFull AMQP queue semantics with explicit acks and requeue
PersistenceRDB/AOF persistence; recovery configurableDurable queues survive broker restarts by default
Operations burdenOne container, minimal tuningMore moving parts: exchanges, bindings, management UI
Best fitMost Airflow fleets; speed and simplicity winHeavy queueing needs: priority, per-queue durability
Double-execution riskvisibility_timeout below task runtime redeliversack timeout on long tasks can redeliver too
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
broker.shdocker run -d --name airflow-redis -p 6379:6379 redis:7-alpine2. Broker Setup
start_worker.shairflow celery worker -H worker@prod-1 \3. Workers, Queues, and Autoscale
airflow-worker.service[Unit]5. Worker Death
airflow.cfg[celery]6. The Visibility-Timeout Double-Execution Trap

Key takeaways

1
CeleryExecutor decouples scheduling from execution via a broker and a worker fleet.
2
Celery never restarts dead workers; supervision via systemd or Kubernetes is mandatory.
3
worker_autoscale provides elasticity but must be sized to host memory.
4
Set the broker visibility timeout above your longest task or accept double executions.
5
Flower plus alerts on queue depth and heartbeats turn silent fleet death into a page.

Common mistakes to avoid

4 patterns
×

Leaving the broker visibility timeout at its default.

Symptom
Tasks longer than the window run twice, both logs show success.
Fix
Set visibility_timeout above your longest task and re-check it whenever a new long task ships.
×

Running static workers with no supervisor.

Symptom
An OOM kill takes the whole fleet down; the queue backs up silently overnight.
Fix
Run workers under systemd with Restart=always, or as Kubernetes Deployments with readiness probes.
×

Setting worker concurrency above host memory.

Symptom
OOM kills under load; tasks are lost or redelivered mid-run.
Fix
Cap concurrency with --autoscale or worker_concurrency sized to host RAM.
×

No alerting on queue depth or worker heartbeats.

Symptom
The backlog grows for hours while nobody is paged.
Fix
Alert on queue length and worker heartbeat in your monitoring stack; watch it in Flower.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the broker in Airflow's Celery setup?
Q02SENIOR
Why might a task run twice with CeleryExecutor, and how do you prevent i...
Q03SENIOR
All tasks are queued, zero workers are online, and there's no OOM in the...
Q01 of 03JUNIOR

What is the broker in Airflow's Celery setup?

ANSWER
The broker is the message queue where task instances wait for workers, Redis or RabbitMQ in practice. The scheduler publishes tasks there, workers consume them, and results go back through the result backend. It decouples scheduling from execution.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Redis or RabbitMQ for Airflow's Celery broker?
02
What does worker_autoscale mean in Airflow?
03
What is a visibility timeout?
04
A worker died. How do I restart it without losing tasks?
05
How do I know my workers are alive?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Verified
production tested
August 01, 2026
last updated
1,750
articles · all by Naren
🔥

That's Airflow. Mark it forged?

3 min read · try the examples if you haven't

Previous
Airflow Docker Compose Setup
23 / 37 · Airflow
Next
Airflow on Kubernetes