Home DevOps Airflow Task States: Stuck in Queued? Here's the Fix
Intermediate 3 min · September 04, 2026
Airflow Task Lifecycle and Retries

Airflow Task States: Stuck in Queued? Here's the Fix

Airflow tasks stuck in queued with retries=0 kill pipelines.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.

Follow
Production
production tested
September 04, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 30 min
  • A DAG you have watched succeed and fail
  • Basic Python timedelta and retry concepts
  • Access to Grid view and task logs
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Task states track every step: queued, running, success, failed, up_for_retry, skipped, up_for_reschedule, deferred
  • retries plus retry_delay and max_retry_delay with exponential backoff turn transient blips into non-events
  • With retries=0, one 3 AM DB hiccup fails the whole DAG and queues everything behind it for hours
  • Production rule: set retry policy at DAG level, monitor queued age as a capacity signal, and tune max_active_tasks
  • Join tasks need explicit trigger rules or skipped branches fail the run
✦ Definition~90s read
What is Airflow Task Lifecycle and Retries?

The task lifecycle is the state machine each task instance travels, from queued through running to success, failure, retry, skip, or deferral.

Airflow tasks move through states like airport flights: queued at the gate, running in the air, retrying after a missed landing with longer waits between attempts, and occasionally rerouted, while the control tower watches each status and only calls you when a flight truly cannot land.
Plain-English First

Airflow tasks move through states like airport flights: queued at the gate, running in the air, retrying after a missed landing with longer waits between attempts, and occasionally rerouted, while the control tower watches each status and only calls you when a flight truly cannot land.

A transient DB blip failed one task at 3 AM. With retries=0 the whole pipeline died, and the page woke a human for an incident a retry would have healed. The fix was three lines of policy.

Task states tell you where every run stands, from queued to deferred. You'll learn which states signal trouble and which mean patience.

We'll map the state machine, honest retries, and concurrency knobs. Queued stops being a mystery.

The Full State Machine (Why It Matters)

Every task instance carries a state: scheduled, queued, running, success, failed, up_for_retry, skipped, up_for_reschedule, or deferred. Scheduled means the dependencies are met. Queued means it waits for a slot. Running means a worker owns it.

Terminal states end the story for that attempt. Success, failed, and skipped close the instance. Up_for_retry and deferred are waiting rooms with different landlords: the clock and the triggerer.

Learn to read the Grid view as sentences. Green success, red failed, grey queued-aging means capacity trouble, not code trouble.

Full 3.x list is 13 states: none, scheduled, queued, running, success, failed, skipped, upstream_failed, up_for_retry, up_for_reschedule, deferred, restarting, removed — plus awaiting_input for human-in-the-loop tasks. You'll clear failed instances (try_number bumps, max_tries resets) from Grid or airflow tasks clear, and history with per-try logs stays inspectable.

📊 Production Insight
Grey spread means capacity, red spread means bugs.
State literacy cuts triage to minutes.
Rule: name the state before touching code.
🎯 Key Takeaway
States turn red squares into specific diagnoses.
Waiting rooms differ: clock, slots, triggerer.
Read the Grid as sentences.

Retries: Honest Retry vs Hiding Bugs

Retries admit that networks blip. retries=3 gives a task three extra chances after the first failure. retry_delay spaces attempts so the dependency can recover. The defaults live in default_args and apply fleet-wide.

Honest retries pair with idempotent tasks. A safe rerun heals; an unsafe rerun duplicates. Set retries only on tasks you have proven re-runnable with a clear-and-rerun test.

Keep alerts on exhaustion, not on attempts. Paging on every first failure pages humans for blips. Paging when retries run out pages humans for real bugs.

Go finer with 3.x ExceptionRetryPolicy: map HTTPError to RETRY with a 5-minute delay, auth errors to FAIL fast, ConnectionError to 30-second RETRY. Rules run in the worker, first match wins, and retries still caps the total. Raise AirflowSkipException for no-data days and AirflowFailException to skip remaining retries on bad credentials.

dags/resilient_sync.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import pendulum
from datetime import timedelta
from airflow.sdk import dag, task

@dag(
    schedule="@daily",
    start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
    catchup=False,
    default_args={
        "owner": "platform",
        "retries": 3,
        "retry_delay": timedelta(minutes=5),
        "max_retry_delay": timedelta(minutes=30),
    },
    tags=["resilience"],
)
def resilient_sync():
    @task
    def pull_warehouse() -> str:
        return "partition-2026-09-03"

    @task
    def publish(partition: str) -> None:
        print(f"publishing {partition}")

    publish(pull_warehouse())

resilient_sync()
📊 Production Insight
retries=0 pages humans for database hiccups.
Exhaustion alerts page for real bugs.
Rule: retry only what reruns safely.
🎯 Key Takeaway
Retries heal blips; idempotency keeps them honest.
Alert on exhaustion, not on attempts.
Prove reruns safe, then retry freely.

Backoff and max_retry_delay

Backoff spaces retries exponentially: 5 minutes, then 10, then 20. Without it, every attempt hammers a dependency that is already down. With it, recovery gets breathing room.

max_retry_delay caps the growth so waits stay sane. A 6-hour cap on an hourly pipeline means attempts never sleep past usefulness. Tune caps to the schedule cadence.

Add jitter by staggering DAG start times across teams. Synchronized retries after a shared outage arrive as a stampede; spread starts turn the stampede into a queue.

Backoff knobs are retry_delay, retry_exponential_backoff, and max_retry_delay — keep caps under one schedule interval so hourly DAGs never sleep past usefulness. You'll also set execution_timeout (AirflowTaskTimeout on breach) per task and timeout on reschedule-mode sensors (AirflowSensorTimeout, no retry). Stagger starts to dodge herds.

📊 Production Insight
Instant retries punish dependencies for failing.
Backoff is politeness with a timer.
Rule: cap delays below one schedule interval.
🎯 Key Takeaway
Exponential waits respect recovering dependencies.
Caps keep waits within schedule sanity.
Stagger starts to dodge synchronized stampedes.

What Queued Means and Why Tasks Get Stuck

Queued means waiting for a worker slot, a pool slot, or a concurrency limit. The task is ready but homeless. Short queues are normal; aging queues are capacity alarms.

Diagnose in order: pool occupancy, executor parallelism, per-DAG max_active_tasks, then greedy neighbors. One DAG with unbounded concurrency can starve ten polite ones.

Fix structurally. Raise pool slots deliberately, cap greedy DAGs, or move long waits to deferred operators. Restarting the scheduler treats the symptom for an hour.

Stuck running often means heartbeat timeout: OOMKill, liveness-probe restart, or node scale-down leaves a zombie the scheduler reaps and fails or retries. Tune task_instance_heartbeat_sec and watch for it after K8s churn. Check airflow tasks states-for-dag-run <dag> <date> to list states fast.

diagnose_queued.shBASH
1
2
3
4
# find who sits queued and why
AIRFLOW_HOME=~/airflow airflow pools list
AIRFLOW_HOME=~/airflow airflow dags list-runs -d sales_daily --limit 10
AIRFLOW_HOME=~/airflow airflow tasks states-for-dag-run sales_daily 2026-09-03
📊 Production Insight
Aging queues precede SLA breaches by hours.
Queued-age alerts buy mornings back.
Rule: alert on queued age, not just failures.
🎯 Key Takeaway
Queued is homelessness, not brokenness.
Pools, parallelism, greedy neighbors: check in order.
Cap the greedy before feeding the cluster.

Per-DAG vs Per-Task Concurrency Knobs

Three knobs bound concurrency at different scopes. parallelism caps the whole scheduler. max_active_tasks caps one DAG. Pools cap arbitrary task groups like warehouse writers.

Set them as a stack. parallelism guards the cluster, max_active_tasks guards the noisy DAG, pools guard the shared downstream. Each layer stops a different stampede.

Review quarterly as DAG counts grow. Defaults that fit 20 DAGs strangle 200. Capacity planning is scheduling work, not hardware shopping.

💡Joins Need Explicit Rules
A join task with default all_success fails when any branch skips. After every branch, set the join rule explicitly and test the skip path before merging.
📊 Production Insight
Uncapped DAGs eat clusters during catchups.
Layered caps contain every stampede shape.
Rule: every DAG declares max_active_tasks.
🎯 Key Takeaway
Cluster, DAG, pool: three scopes, three caps.
Defaults age badly as DAG counts grow.
Plan concurrency like latency budgets.

Trigger Rules: The Quiet Killer

Trigger rules decide which upstream states release a task. all_success needs every upstream green. none_failed tolerates skips. all_done runs no matter what. The default is all_success.

That default quietly kills joins after branches. One skipped path means not all succeeded, so the join fails on a healthy run. The DAG goes red over a path that was designed to be optional.

Set join rules deliberately and test skip scenarios with dags test. Branching gets its deep dive later; for now, remember the join is where defaults bite.

dags/guarded_join.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import pendulum
from airflow.sdk import dag, task
from airflow.sdk.bases.operator import chain

@dag(
    schedule="@daily",
    start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
    catchup=False,
    tags=["branching"],
)
def guarded_join():
    @task.branch
    def pick() -> str:
        return "heavy_path"

    @task
    def heavy_path() -> str:
        return "heavy done"

    @task
    def light_path() -> str:
        return "light done"

    @task(trigger_rule="none_failed")
    def join(results: str) -> None:
        print(f"joined: {results}")

    branch = pick()
    join(branch)

    heavy = heavy_path()
    light = light_path()
    chain(branch, heavy, light)

guarded_join()
📊 Production Insight
Red joins on green branches mean rule bugs.
none_failed is the common join fix.
Rule: never merge a branch without a join test.
🎯 Key Takeaway
Defaults assume no branches; branches break defaults.
Joins declare their tolerance explicitly.
Test the skip path, not just happy path.
● Production incidentPOST-MORTEMseverity: high

Tasks Stuck in Queued Killed the Pipeline

Symptom
The Grid showed 6 red squares and 40 grey queued squares by 3:20 AM across 3 DAGs sharing one pool. Queued age passed 240 minutes with workers half-idle behind failed upstreams. On-call woke at 6:40 AM to stale dashboards that one retry at 3:08 would've healed.
Assumption
The team assumed their Postgres never blips and that retries hide real bugs, so retries=0 felt like honesty. They believed every failure deserved a human at 3 AM rather than a second chance 5 minutes later. Nobody'd measured that transient timeouts hit roughly twice a week.
Root cause
With retries=0, no retry_delay, and no backoff, a 90-second connection hiccup became 6 terminal failures instead of 6 brief up_for_retry waits. Downstream tasks then queued behind failed upstreams per default all_success rules. No queued-age alert existed, so the stall sat invisible for 3.5 hours until the SLA breached.
Fix
They set default_args to retries=3, retry_delay=timedelta(minutes=5), max_retry_delay=timedelta(minutes=30) with exponential backoff, plus max_active_tasks=8 on the greedy DAG. They diagnosed capacity with airflow pools list and airflow tasks states-for-dag-run warehouse_sync 2026-09-03, and alerted on queued age over 10 minutes. 3 AM pages dropped to zero in 9 days.
Key lesson
  • Transient failures strike weekly; retries=3 with capped backoff turns them into non-events.
  • Backoff with caps heals without stampeding a recovering DB with 40 instant retries.
  • Monitor queued age past 10 minutes as a capacity signal, not just failed states.
Production debug guideStates name the problem; these actions close it.4 entries
Symptom · 01
Tasks stuck in queued for hours
Fix
Run airflow pools list and airflow dags list-runs to find saturation. Raise the pool slots or lower the greedy DAG's max_active_tasks, then watch queued age fall in the next runs.
Symptom · 02
Task failed with retries=0 overnight
Fix
Read the task log tail for the fatal line, fix the cause, then run airflow tasks clear dag_id -t task_id --yes to rerun just that step (try_number bumps). For auth errors raise AirflowFailException so retries don't waste cycles. Confirm policy exists so blips heal next time.
Symptom · 03
Retry storm hammers a recovering dependency
Fix
Check default_args for missing retry_delay and add retry_delay plus max_retry_delay with exponential backoff. Stagger DAG start times so shared-dependency outages do not synchronize retries.
Symptom · 04
Join task fails after a branch skips
Fix
Open the Graph view, find the red join task, and change its trigger_rule to none_failed. Re-run the skip scenario with airflow dags test before merging.
★ Task State Debug Cheat SheetState diagnosis commands that separate capacity trouble from task bugs in minutes.
Tasks sit in queued for hours
Immediate action
Check pool and executor saturation before blaming task code
Commands
AIRFLOW_HOME=~/airflow airflow pools list
AIRFLOW_HOME=~/airflow airflow dags list-runs -d my_dag --limit 10
Fix now
Raise pool slots or cut the greedy DAG's max_active_tasks, then watch queued age recover.
Task failed overnight with no retries+
Immediate action
Read the failed task's log tail for the fatal line
Commands
AIRFLOW_HOME=~/airflow airflow tasks logs my_dag my_task 2026-09-03 --tail 50
AIRFLOW_HOME=~/airflow airflow tasks clear my_dag -t my_task --yes
Fix now
Fix the cause, clear just that task instance, and add retries so blips heal.
Retries stampede a recovering dependency+
Immediate action
Confirm backoff policy exists in the DAG file
Commands
grep -rn 'retries\|retry_delay' dags/my_dag.py
AIRFLOW_HOME=~/airflow airflow dags show my_dag | head -30
Fix now
Add retry_delay with exponential backoff and max_retry_delay caps at DAG level.
Join task goes red after a branch skips+
Immediate action
Reproduce the skip path with a single test run
Commands
AIRFLOW_HOME=~/airflow airflow dags test my_dag 2026-09-03 2>&1 | tail -20
grep -rn 'trigger_rule' dags/my_dag.py
Fix now
Set the join trigger_rule to none_failed and re-test the skip scenario.
Task States at a Glance
StateMeaningNeeds attention?Typical fix
queuedWaiting for a worker slotYes if aged past minutesRaise slots or cut greedy DAGs
runningExecuting on a workerYes if past timeoutCheck logs, kill zombies
successCompleted cleanlyNoNone
failedExhausted retries or fatalYes, alwaysRead log, fix, clear
up_for_retryWaiting between attemptsNo unless loopingVerify backoff caps
skippedBranch or guard skipped itYes on join tasksSet join trigger rule
deferredParked in triggererNo, cheap waitingScale triggerer if lagging
upstream_failedUpstream failed, rule needed itYesFix upstream or change rule
⚙ Quick Reference
3 commands from this guide
FileCommand / CodePurpose
dagsresilient_sync.pyfrom datetime import timedeltaRetries
diagnose_queued.shAIRFLOW_HOME=~/airflow airflow pools listWhat Queued Means and Why Tasks Get Stuck
dagsguarded_join.pyfrom airflow.sdk import dag, taskTrigger Rules

Key takeaways

1
The 13-state machine (none through removed plus awaiting_input) turns midnight mysteries into named states with per-try history.
2
Retries with capped backoff plus 3.x ExceptionRetryPolicy heal blips and fail fast on auth; retries=0 pages humans for hiccups.
3
Queued-aging is a capacity signal
pools, parallelism, max_active_tasks, greedy DAGs — confirm with states-for-dag-run.
4
execution_timeout plus sensor timeout bound runaway tasks; heartbeat timeouts reap zombies after OOM or reschedules.
5
Join tasks need explicit trigger rules (none_failed/all_done) or skipped branches fail the run
test the skip path.

Common mistakes to avoid

4 patterns
×

Shipping retries=0 on tasks that touch networks

Symptom
Every DB blip or API timeout fails the whole DAG; on-call pages for incidents that a retry would have healed.
Fix
Set retries=3 with retry_delay plus max_retry_delay at DAG level, add execution_timeout per long task, and use ExceptionRetryPolicy for per-exception routing. Transient blips heal; auth bugs still fail fast.
×

Retrying instantly with no backoff or delay caps

Symptom
A downed dependency receives a retry stampede that delays its recovery; logs show thousands of attempts in minutes.
Fix
Add retry_delay=timedelta(minutes=5) with exponential_backoff=True and max_retry_delay caps. Jitter spreads thundering herds after shared outages.
×

Ignoring queued-state bottlenecks until SLAs breach

Symptom
Tasks sit queued for hours while workers look busy; one greedy DAG starves every other pipeline.
Fix
Raise parallelism and pool slots deliberately, or cut max_active_tasks on greedy DAGs. Monitor queued age, not just queue length.
×

Leaving default all_success on join tasks after branches

Symptom
Skipped branches fail the join task; the DAG goes red on a path that was supposed to be optional.
Fix
Set the join task's trigger rule to none_failed or all_done explicitly and test with a skip scenario in Graph view before merging.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Describe the task state machine and retry knobs.
Q02SENIOR
Tasks pile up in queued for hours. How do you diagnose it?
Q03SENIOR
How do you design retries that heal without hiding bugs?
Q01 of 03JUNIOR

Describe the task state machine and retry knobs.

ANSWER
Tasks flow through scheduled, queued, running, then success, failed, or skipped, with up_for_retry between attempts and deferred while parked in the triggerer. retries plus retry_delay and max_retry_delay with backoff turn blips into non-events. Queued-aging signals capacity trouble; I check pools, parallelism, and max_active_tasks before blaming task code.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What does queued actually mean?
02
How do retries, retry_delay, and max_retry_delay fit together?
03
What is the difference between up_for_retry and failed?
04
What is a trigger rule in one paragraph?
05
Why do deferred tasks matter for cost?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
September 04, 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 Scheduling and Catchup
6 / 37 · Airflow
Next
Airflow XComs for Data Passing