Home DevOps Airflow Task States: Stuck in Queued? Here's the Fix
Intermediate 4 min · August 1, 2026
Airflow Task Lifecycle

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

Airflow task states explained: queued, running, failed, retries, and backoff.

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
August 01, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 15-20 min read
  • Airflow installed with a DAG you can run and observe in the grid view.
  • Understanding of basic task failures and how a DAG run progresses.
  • Familiarity with airflow.cfg and environment-based configuration.
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Task instances move through a state machine: none, scheduled, queued, running, success, failed, up_for_retry, skipped, upstream_failed, up_for_reschedule, and deferred.
  • Key components: retries, retry_delay, max_retry_delay, max_active_tasks, and per-DAG concurrency knobs.
  • A task with retries=0 dies on the first transient blip — one DB hiccup took down our whole nightly pipeline.
  • Queued is the waiting room: tasks sit there until a worker slot frees up, so a full pool looks like a dead scheduler.
  • Production rule: retries=3 with exponential backoff, and alert on queued-task age before you alert on failures.
  • A task left stuck in queued is eventually failed by the scheduler — and that failure consumes a retry, so a capacity bottleneck quietly burns your retry budget.
✦ Definition~90s read
What is Airflow Task Lifecycle?

The Airflow task lifecycle is the state machine every task instance moves through — queued, running, success, failed, up_for_retry, skipped, up_for_reschedule, and deferred — governed by retries, backoff, and concurrency settings.

A task instance is a to-do item with a sticky note showing its mood.
Plain-English First

A task instance is a to-do item with a sticky note showing its mood. Queued means it's in the waiting room; running means someone is doing it; failed means it broke; up_for_retry means it broke but gets another shot. Retries are how many second chances you allow, and backoff is how long you wait between attempts. Our pipeline gave tasks zero second chances, so the first database hiccup killed everything. Give tasks a few chances with a sensible pause between them and a pipeline survives the messy real world.

Every Airflow UI shows the same color-coded lie: green tasks are fine, red tasks are not. The truth lives in the states in between. Queued is where pipelines quietly die — tasks waiting for a worker slot that never comes, while the DAG run hangs for hours. Failed with no retry is where pipelines die loudly.

Our nightly pipeline failed on a single transient database blip. The task had retries=0 and no backoff, so one 500 from Postgres turned a 20-minute run into a full failure. The fix wasn't more compute. It was a retry policy and an understanding of what queued actually means.

This article maps the full task state machine, then shows you the retry and concurrency knobs that keep it healthy. States aren't trivia. They're the diagnostics that tell you whether your pipeline is slow, stuck, or actually broken.

1. The Full State Machine (and Why It Matters)

A task instance starts life in queued — the scheduler has decided to run it, but a worker hasn't picked it up yet. From there it can go running, then success, failed, or skipped. Failed hands the baton to up_for_retry when retries remain. Up for reschedule and deferred are the waiting states for sensors and deferrable operators.

Every state is a diagnostic. Queued says "no capacity." Up for retry says "a transient failure happened, and we're waiting to try again." Skipped says "this task's trigger rule decided it had nothing to do." A state is never just a color.

Read the grid view like a doctor reads vitals: a run full of green with one yellow up_for_retry is healthy; a run full of queued is a capacity problem wearing a scheduler's costume.

One refinement from the docs' canonical flow: a fresh task instance actually starts in none (dependencies not yet met), moves to scheduled when the scheduler determines it should run, and only then to queued, when the executor accepts the task and it awaits a worker. Three more states complete the picture: restarting (a running task cleared or restarted externally), upstream_failed (an upstream task failed and the trigger rule requires it), and removed (the task vanished from the DAG after the run started).

📊 Production Insight
Every state is a diagnostic, not a color.
Queued means no capacity; up_for_retry means a transient failure.
Read the grid view like vitals, not like a traffic light.
🎯 Key Takeaway
Task instances are state machines, and states are signals.
Success and failure are the only endpoints that finish a run.
Learn what each state means before the dashboard teaches you.
The Airflow Task State Machine The Airflow Task State Machine Every state is a diagnostic, not a color queued waiting for a worker slot running worker is executing success outputs pushed to XCom failed no retries left up_for_retry waiting out the delay skipped trigger rule said no Sensors and deferrable operators up_for_reschedule sensor between pokes deferred waiting on the triggerer THECODEFORGE.IO
thecodeforge.io
Airflow Task Lifecycle

2. Retries: Honest Retry vs Hiding Bugs

Retries exist for transient failures: network blips, connection timeouts, database 500s, race conditions with an upstream job. They exist to absorb noise, not to launder bugs. A deterministic failure — a typo in SQL, a missing column — will fail the same way on retry 1 and retry 10.

The line between honest retry and hiding bugs is drawn by your logs. If three retries all fail at the same line of code, you're paying for a bug, not a blip. Check the logs after the first retry, not after the third.

Our incident pipeline had retries=0, which made every transient failure fatal. The fix was retries=3 — enough to absorb a blip, few enough that a real bug surfaces in under an hour.

Airflow also ships the honest-retry escape hatch: raise AirflowFailException inside a task to mark it failed and skip any remaining retries, or AirflowSkipException to skip it outright — the right call when the failure is definitively not transient, like a bad input record. Airflow 3's retry policies go further, letting you attach per-exception retry behavior that returns RETRY, FAIL, or DEFAULT actions from the task worker process without touching task code.

daily_revenue_rollup.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from datetime import datetime
from airflow import DAG
from airflow.operators.python import PythonOperator

with DAG(
    dag_id="daily_revenue_rollup",
    schedule="@daily",
    start_date=datetime(2026, 8, 1),
    catchup=False,
    default_args={
        "retries": 3,
        "retry_delay": timedelta(minutes=5),
    },
    tags=["billing"],
) as dag:
    def aggregate_revenue():
        # transient DB blips get retried; real bugs surface fast
        ...

    aggregate_task = PythonOperator(
        task_id="aggregate_revenue",
        python_callable=aggregate_revenue,
    )
Output
2026-08-01T06:05:00+00:00 INFO - Attempt 1/3 failed: connection reset
2026-08-01T06:10:00+00:00 INFO - Attempt 2/3 succeeded
⚠ Retries Hide Bugs
Three identical failures mean you're not retrying a blip — you're replaying a bug. If retry logs show the same traceback three times, fix the code, don't raise retries.
📊 Production Insight
Retries absorb transient failures, not bugs.
A deterministic failure fails identically every attempt.
Check logs after the first retry, not the third.
🎯 Key Takeaway
retries=0 makes every blip fatal; that's how pipelines die.
retries=10 hides real bugs behind a clock.
Set retries=3 and read the retry logs.
Retries: Absorbing Blips, Not Bugs Retries: Absorbing Blips, Not Bugs retries=0 killed the run; retries=3 absorbed it retries=0 — every blip fatal the incident default retries=3 — absorbs noise the production fix Transient DB blip Fails outright — no retry Attempt 2 succeeds Deterministic bug Fails once, loudly Same traceback x3 Incident: Postgres 500 Nightly run killed 5-min delay, then green Three identical failures = a bug fix the code, not the retry count THECODEFORGE.IO
thecodeforge.io
Airflow Task Lifecycle

3. Backoff and max_retry_delay

Retry count without delay means a hammering thundering herd: three tasks all failing on the same downstream outage, retrying in lockstep every 30 seconds. Backoff is what spreads the attempts out. Airflow's exponential backoff doubles the wait each round: 5 minutes, then 10, then 20.

max_retry_delay is the ceiling. It caps the doubling so a long chain of retries can't stretch into a multi-hour wait. Set it to the maximum time you're willing to let a task sit in up_for_retry before it either succeeds or fails for good.

A sane production default: retries=3, retry_delay=5 minutes, exponential backoff, max_retry_delay=1 hour. That's a worst case of about 35 minutes of waiting for a task that genuinely needs the world to calm down.

daily_revenue_rollup.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from datetime import timedelta

with DAG(
    dag_id="daily_revenue_rollup",
    schedule="@daily",
    start_date=datetime(2026, 8, 1),
    catchup=False,
    default_args={
        "retries": 3,
        "retry_delay": timedelta(minutes=5),
        "retry_exponential_backoff": True,
        "max_retry_delay": timedelta(hours=1),
    },
) as dag:
    ...
Output
retry 1: +5m | retry 2: +10m | retry 3: +20m (capped at 1h)
📊 Production Insight
Backoff spreads retries; without it you get thundering herds.
max_retry_delay caps the wait so retries can't drag for hours.
5m, 10m, 20m is the shape of a sane retry policy.
🎯 Key Takeaway
retry_delay alone is not backoff.
Exponential backoff doubles the wait; max_retry_delay caps it.
Time the retry ladder to the outage window you expect.
Exponential Backoff: 5m, 10m, 20m Exponential Backoff: 5m, 10m, 20m Spreading retries so failures don't thunder in lockstep No backoff — the herd three tasks, one outage, retrying in lockstep every 30 seconds Exponential backoff retry 1 — wait 5 min retry 2 — wait 10 min retry 3 — wait 20 min max_retry_delay = 1 hour cap Worst case ≈ 35 min of waiting spread over 3 retries, capped at 1 hour Production default retries=3 · backoff · 1 h cap THECODEFORGE.IO
thecodeforge.io
Airflow Task Lifecycle

4. What "Queued" Means — and Why Tasks Get Stuck

Queued is the waiting room. The scheduler has committed to the run, but no worker slot is free. With the default SequentialExecutor that's one slot total. With Local or Celery executors, slots are bounded by parallelism, pools, and max_active_tasks.

Tasks get stuck in queued for exactly four reasons: no free worker slots (parallelism saturated), a pool with zero free slots, a max_active_tasks ceiling on the DAG, or a scheduler that stopped heartbeating. Diagnose in that order and you'll find it every time.

Our queued pile-up was capacity, not breakage: the DAG's tasks were all competing for a pool sized for half the workload, so late runs queued behind earlier ones and never recovered. The fix was concurrency tuning, not code changes.

One gotcha the docs and scheduler code make explicit: a task stuck in queued is not parked forever. The scheduler eventually fails tasks that linger in queued too long, and that failure counts against the task's retries — so a capacity bottleneck quietly eats your retry budget while the run looks merely 'slow'.

airflow.cfgINI
1
2
3
4
5
6
7
[core]
parallelism = 32            # total task instances across the whole deployment
max_active_tasks_per_dag = 16
max_active_runs_per_dag = 4  # DAG runs per DAG that can be active at once

[scheduler]
max_tis_per_query = 16       # task instances the scheduler schedules per loop
Output
parallelism=32, max_active_tasks_per_dag=16, max_active_runs_per_dag=4
Mental Model
Queued Is a Waiting Room
Queued is a capacity signal, not a failure signal — a full pool looks exactly like a dead scheduler.
  • Tasks queue because no worker slot is free, not because they're broken.
  • Pools, parallelism, and max_active_tasks are three separate gates — any one full queues tasks.
  • Alert on queued-task age; a queue that grows while heartbeats are healthy is a capacity problem.
📊 Production Insight
Queued is the waiting room, not a failure state.
Four gates queue tasks: slots, pools, per-DAG limits, heartbeats.
Diagnose in that order and you'll find the stuck task every time.
🎯 Key Takeaway
A full pool looks exactly like a dead scheduler.
Check capacity before you suspect breakage.
Monitor queued-task age, not just failure counts.

5. Per-DAG vs Per-Task Concurrency Knobs

Concurrency in Airflow is a stack, not a single switch. At the top, [core] parallelism caps total running task instances for the whole deployment. Below it, max_active_tasks_per_dag caps each DAG, max_active_runs_per_dag caps concurrent runs of the same DAG, and pools cap specific task groups regardless of DAG.

The confusion comes when people set one knob expecting it to do another's job. Raising parallelism won't help if a pool is the bottleneck. Raising the pool won't help if the DAG's max_active_tasks is the ceiling.

The rule: reason top-down. Slots exhausted → parallelism. One DAG hogging everything → max_active_tasks_per_dag. A specific external system being hammered → a pool with a priority_weight policy.

pool_definition.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from airflow import DAG
from airflow.operators.python import PythonOperator

# Cap API-heavy tasks across all DAGs at 4 concurrent instances
with DAG(
    dag_id="daily_revenue_rollup",
    schedule="@daily",
    start_date=datetime(2026, 8, 1),
    catchup=False,
    default_args={"pool": "external_api_quota"},
) as dag:
    def call_external_api():
        ...

    api_task = PythonOperator(
        task_id="call_external_api",
        python_callable=call_external_api,
    )
# create the pool: airflow pools set external_api_quota 4 "API calls quota"
Output
Pool external_api_quota: 4 slots, 0 in use
📊 Production Insight
Concurrency is a stack, not a switch.
Parallelism, per-DAG caps, and pools are separate gates.
Reason top-down: slots, then DAG, then pool.
🎯 Key Takeaway
One knob can't fix a three-layer bottleneck.
Match the knob to the gate that's actually full.
Pools protect external systems; parallelism protects your fleet.

6. Trigger Rules: The Quiet Killer (Intro)

A task's trigger rule decides when it may start after its upstream tasks finish. The default is all_success — every upstream task must succeed. That's correct for linear pipelines and quietly fatal for anything with branches or skips.

Skipped tasks fail all_success joins. A sensor that soft-fails, a branch you didn't take, a downstream task of either — all of them make the join fail even though nothing actually broke. That's next article's deep dive, but the intro belongs here: trigger rules are states' second half.

The state machine doesn't end at the task. It ends at the DAG's trigger rules deciding whether the graph continues. When a run fails with green upstream tasks, look at the trigger rule, not the code.

📊 Production Insight
Trigger rules decide when a task may start.
all_success fails on skipped branches — the quiet killer.
Green upstreams with a red join mean a rule problem, not a code problem.
🎯 Key Takeaway
The state machine ends at trigger rules, not at tasks.
Skipped tasks break all_success joins by design.
Learn trigger rules before you write your first branch.

7. Monitoring Task States in Production

States only help if someone is watching. The metric that matters most is queued-task age — how long tasks wait for a slot. A rising queue with healthy scheduler heartbeats is a capacity alert. A queue that grows while heartbeats stop is a dead scheduler alert. Two very different on-call pages from one metric.

Failure rate is the second metric, but count it honestly: failed-with-retry-remaining is noise, failed-final is signal. Airflow's own metrics and the task instance list in the UI give you both, and exporting them to Prometheus turns states into trends.

The discipline that ends incidents: a weekly check of the grid view, an alert on queued age, and a retry policy that's visible in the DAG code. Monitoring isn't a dashboard. It's a habit.

📊 Production Insight
Queued-task age is the metric that matters most.
Retries remaining is noise; failed-final is signal.
Alert on queue growth before you alert on failures.
🎯 Key Takeaway
States become trends when you export them.
A rising queue means capacity; a silent scheduler means death.
Watch the waiting room, not just the graveyard.
● Production incidentPOST-MORTEMseverity: high

The Queued Pile-Up That Killed the Pipeline

Symptom
Tasks piled up in queued for hours while the nightly run never completed. When the queue finally moved, a transient database blip failed a task outright — and with retries=0, the entire DAG stopped.
Assumption
The team assumed retries were enabled by default and that a failure on the next scheduled run would simply recover the data. Neither was true.
Root cause
The task declared retries=0 and no retry_delay, so a single transient DB blip failed the run with no second chance. Meanwhile, the queued backlog looked like a scheduler problem but was really a capacity problem: no concurrency tuning and no queued-task monitoring.
Fix
Set retries=3, retry_delay=5 minutes, and max_retry_delay=1 hour at the DAG and task level with exponential backoff, right-sized the concurrency knobs, and started monitoring queued-state bottlenecks.
Key lesson
  • retries=0 means the first transient blip is a fatal failure — the DB hiccup was the last straw, not the cause.
  • Queued is a capacity signal: tasks wait because no worker slot is free, not because they're broken.
  • Exponential backoff with a max_retry_delay turns 3 retries into minutes of interruption, not an hour of delay.
  • Monitor queued-task age like a metric, because a full pool looks exactly like a dead scheduler.
Production debug guideSymptom to Action5 entries
Symptom · 01
Task stuck in queued for hours
Fix
Check the executor's parallelism, pool occupancy, and scheduler heartbeat; a full pool queues tasks even when the cluster is healthy.
Symptom · 02
Task failed with no retry fired
Fix
Render the task's params with airflow tasks render and verify retries in default_args; retries=0 is the usual culprit.
Symptom · 03
Task retries forever without giving up
Fix
Review retry_delay and max_retry_delay; a deterministic failure will retry on schedule and never succeed until the code is fixed.
Symptom · 04
Tasks run one at a time despite multiple DAGs
Fix
Check max_active_tasks_per_dag and parallelism; defaults can serialize work you expected to run in parallel.
Symptom · 05
Task sits in up_for_retry past its delay
Fix
Clear the instance with airflow tasks clear to retry now, or shorten retry_delay; a stale retry window usually means no scheduler heartbeat.
★ Quick Debug Cheat SheetCommon task-state issues and immediate actions.
Task stuck in queued
Immediate action
Check slots and heartbeats
Commands
airflow tasks states daily_revenue_rollup aggregate_revenue 2026-07-15
airflow config get-value core parallelism
Fix now
Free worker slots: raise parallelism or shrink the backlog.
Task failed, no retry fired+
Immediate action
Render the task params
Commands
airflow tasks render daily_revenue_rollup aggregate_revenue 2026-07-15
airflow tasks test daily_revenue_rollup aggregate_revenue 2026-07-15
Fix now
Add retries=3 and retry_delay to default_args.
Task stuck in up_for_retry+
Immediate action
Clear the instance to retry now
Commands
airflow tasks clear daily_revenue_rollup --task-regex aggregate_revenue
airflow tasks states daily_revenue_rollup aggregate_revenue 2026-07-15
Fix now
Clear it, or shorten retry_delay so retries fire sooner.
Everything queues during peak load+
Immediate action
Check per-DAG limits
Commands
airflow config get-value core max_active_tasks_per_dag
airflow dags list-runs daily_revenue_rollup
Fix now
Stagger schedules or raise max_active_tasks_per_dag deliberately.
Task States at a Glance
StateMeaningWhat to do
queuedScheduler committed; no worker slot free yetCheck parallelism, pools, and scheduler heartbeat
runningA worker is executing the taskWatch duration; alert when it exceeds expected runtime
successTask completed; outputs pushed to XComNothing — verify downstream dependencies fired
failedTask ended in failure with no retries leftRead the logs; fix the code or the data
up_for_retryFailed but retries remain; waiting out the delayWait, or clear the instance to retry now
skippedTrigger rule decided the task had nothing to doVerify the skip was intended; check join rules
up_for_rescheduleReschedule-mode sensor waiting for the next pokeCheck the sensor timeout and schedule cadence
deferredDeferrable operator waiting on the triggererScale the triggerer if deferred tasks pile up
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
daily_revenue_rollup.pyfrom datetime import datetime2. Retries
daily_revenue_rollup.pyfrom datetime import timedelta3. Backoff and max_retry_delay
airflow.cfg[core]4. What "Queued" Means
pool_definition.pyfrom airflow import DAG5. Per-DAG vs Per-Task Concurrency Knobs

Key takeaways

1
Task instances are a state machine; every state is a diagnostic for a distinct failure mode.
2
retries=0 makes transient blips fatal
set retries=3 with retry_delay at the DAG level.
3
Exponential backoff with max_retry_delay spreads retries and caps the wait.
4
Queued is a capacity signal
check parallelism, pools, and per-DAG limits before suspecting the scheduler.
5
Monitor queued-task age, not just failures
a full pool looks exactly like a dead scheduler.

Common mistakes to avoid

4 patterns
×

Leaving retries=0 as the default

Symptom
A single transient DB or network blip fails the whole run
Fix
Set retries=3 with retry_delay in default_args so every task inherits the policy.
×

Setting retries=10 "to make it reliable"

Symptom
Deterministic bugs retry for hours, hiding the real failure and delaying the alert
Fix
Keep retries small (2-3) and treat repeated identical failures as a code bug, not bad luck.
×

Ignoring pools and concurrency knobs

Symptom
Tasks pile up in queued while the run hangs for hours
Fix
Right-size parallelism, max_active_tasks_per_dag, and pools; alert on queued-task age.
×

Never tuning retry_delay or max_retry_delay

Symptom
Retries hammer downstream systems in lockstep, or wait far longer than the outage window
Fix
Use exponential backoff with a 5-minute start and a 1-hour max_retry_delay.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Walk me through the main task states in Airflow.
Q02SENIOR
A task is stuck in queued for three hours. Walk me through your diagnosi...
Q03SENIOR
Design a retry policy for a flaky external API task that must also surfa...
Q01 of 03JUNIOR

Walk me through the main task states in Airflow.

ANSWER
A task instance goes queued (waiting for a worker slot), then running (executing on a worker), then success or failed. Failed becomes up_for_retry if retries remain. Skipped means a trigger rule decided the task shouldn't run. Sensors add up_for_reschedule and deferrable operators add deferred. Each state maps to a distinct diagnosis.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Why is my task stuck in queued forever?
02
What is the difference between failed and up_for_retry?
03
How do retries and retry_delay interact?
04
Should I use max_active_tasks_per_dag or a pool?
05
What does up_for_reschedule mean and how is it different from queued?
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
August 01, 2026
last updated
1,750
articles · all by Naren
🔥

That's Airflow. Mark it forged?

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

Previous
Airflow Scheduling
6 / 37 · Airflow
Next
Airflow XComs