Home DevOps Airflow Backfill: Taming the 90-Day Catchup Storm Safely
Advanced 3 min · August 1, 2026

Airflow Backfill: Taming the 90-Day Catchup Storm Safely

Airflow backfill and catchup explained: catchup=True, the backfill CLI, safe reruns, and the checklist that keeps production backfills from double-loading..

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Production
production tested
August 01, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 15-20 min read
  • Scheduling semantics: start_date, schedule, and data intervals (see airflow-scheduling).
  • Task states and reruns basics (see airflow-task-lifecycle).
  • A production DAG with a load task you can make idempotent.
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Catchup is Airflow backfilling every missed data interval between start_date and now — automatically.
  • catchup=True with a 90-day-old start_date queues 90 DAG runs at once and can flatten your workers.
  • The backfill CLI (airflow dags backfill) targets a date range and limits concurrency per run.
  • Clearing task instances re-runs them; safe reruns depend on idempotent tasks.
  • Production rule: catchup=False in prod, backfill deliberately with explicit ranges.
  • Airflow 3 renamed the CLI to airflow backfill create (--from-date/--to-date/--reprocess-behavior) and added a Trigger → Backfill form in the UI.
✦ Definition~90s read
What is Airflow Backfill and Catchup?

Airflow backfill and catchup are the mechanisms that fill missed data intervals — catchup automatically, backfill deliberately via the CLI.

Imagine a newspaper that missed 90 days of editions.
Plain-English First

Imagine a newspaper that missed 90 days of editions. Catchup is the printer frantically printing all 90 at once the moment it's turned back on. Backfill is you calmly telling the printer: print only March, one edition at a time, and skip the ones we already have. Both print old editions — one floods the building, the other fills the archive safely.

Every Airflow engineer learns the catchup lesson the hard way: a DAG deployed with a start_date six months in the past and catchup=True, and the scheduler suddenly queues 180 runs that hammer the database, the API, and the warehouse simultaneously.

Catchup isn't evil — it's a mechanism. The problem is deploying it by accident and running it without limits.

Here's what catchup actually does, the backfill CLI that gives you control, safe rerun patterns, and the production checklist that keeps a backfill from becoming an incident.

1. What Catchup Actually Does

Airflow schedules one DAG run per data interval between start_date and now. Catchup is the setting that decides what happens to intervals you missed while the DAG was paused, undeployed, or failing.

With catchup=True, the scheduler queues runs for every missed interval as fast as it can. With catchup=False, it skips the missed intervals entirely and runs only the most recent one.

Modern Airflow defaults to catchup=False, but the setting can be flipped per DAG — and that's where incidents come from.

Mental Model
Intervals, not dates
Each run belongs to a data interval, not a wall-clock moment — catchup is just Airflow catching up on intervals.
  • A daily DAG gets one run per day between start_date and now
  • Missed intervals are 'pending' until catchup or backfill handles them
  • catchup=True queues them automatically, all at once
  • catchup=False skips them silently — which is its own trap
📊 Production Insight
catchup=False also hides history: deploy a new DAG and it silently has no data for last month.
Silent gaps get caught by dashboards, not by errors.
Rule: choose catchup by intent — fill the gaps deliberately or skip them deliberately.
🎯 Key Takeaway
Catchup = auto-backfill of every missed interval.
True floods, False forgets — pick deliberately.
Catchup: Missed Intervals, Two Paths Catchup: Missed Intervals, Two Paths One run per interval — true floods, false forgets Scheduler queues one run per interval Between start_date and now Missed intervals wait pending DAG paused, undeployed, or failing catchup=True Auto-queues every missed interval 90 runs queue at once catchup=False Skips the missed intervals Runs the latest interval Modern default: catchup=False Set it per DAG — deliberately THECODEFORGE.IO
thecodeforge.io
Airflow Backfill Catchup

2. The Backfill CLI

The backfill CLI gives you manual control over missed intervals: an explicit date range, a concurrency limit, and a reset flag for reruns.

The core command targets a range and caps concurrent runs. Add --reset-db-runs to re-run intervals that already have run records — the command that turns "redo the failed ones" into one line.

The range semantics matter: --start-date is inclusive, --end-date is exclusive. Mistake the boundary and you either re-run a day you didn't want or skip the day you needed.

Airflow 3 renamed the command and added flags: airflow backfill create --dag-id market_etl --from-date 2026-03-01 --to-date 2026-03-31 --reprocess-behavior failed --max-active-runs 4 --run-backwards--reprocess-behavior picks which existing runs to redo, and --run-backwards processes the range in reverse order, most recent interval first. The same form exists in the UI: DAG Details → Trigger → Backfill.

backfill_commands.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Backfill March only, max 4 concurrent runs
airflow dags backfill market_etl \
  --start-date 2026-03-01 \
  --end-date 2026-03-31 \
  --max-active-runs 4

# Re-run intervals that already ran (after a bug fix)
airflow dags backfill market_etl \
  --start-date 2026-03-01 \
  --end-date 2026-03-31 \
  --reset-db-runs \
  --max-active-runs 2

# See which runs a backfill created
airflow dags list-runs -d market_etl
📊 Production Insight
The default backfill has no concurrency cap — it inherits DAG-level settings.
Large ranges without --max-active-runs reproduce the storm.
Rule: every production backfill passes --max-active-runs and an explicit range.
🎯 Key Takeaway
Backfill CLI = explicit range + concurrency cap + reset.
Never backfill production without all three.
The Backfill CLI: Controlled Catchup The Backfill CLI: Controlled Catchup Explicit range, capped concurrency, reset flag The backfill CLI Manual control over missed intervals Explicit date range Inclusive start · exclusive end Concurrency cap --max-active-runs 4 Reset flag for reruns --reset-db-runs re-runs existing runs The 90-day gap, filled Capped concurrency — no storm THECODEFORGE.IO
thecodeforge.io
Airflow Backfill Catchup

3. Clearing Task Instances Safely

Clearing is the surgical rerun tool: airflow tasks clear resets task instances to their initial state so the scheduler re-queues them.

Scope it narrowly. Clear one task across a date range, or one interval, or one task instance in the grid view. The common production mistake is clearing with a wide range and letting everything re-run — including tasks that never failed.

Clearing does not delete data. It resets Airflow's bookkeeping. Whether the re-run double-loads the warehouse depends entirely on whether your tasks are idempotent.

The mechanics are precise: clearing sets the task instance's state back to None and resets max_tries to 0, which forces the scheduler to re-queue it — and the Grid view preserves the previous attempts as task instance history. Scope flags exist for every rerun shape: --past, --future, --upstream, --downstream, --recursive, and --failed, which re-runs only the instances that actually failed.

clear_commands.shBASH
1
2
3
4
5
6
7
8
9
10
# Clear one task across a date range (re-run just that task)
airflow tasks clear market_etl --task-id load_market_data \
  --start-date 2026-03-01 --end-date 2026-03-15

# Clear all tasks for one interval
airflow tasks clear market_etl \
  --start-date 2026-03-08 --end-date 2026-03-08

# Clear a failed task instance and nothing else (UI equivalent)
airflow tasks clear market_etl --task-id hit_polygon_api --downstream
⚠ Clear is a rerun trigger
  • Clearing resets state; the scheduler re-queues immediately
  • --downstream re-runs everything after the cleared task
  • Wide clears re-run healthy tasks and healthy data loads
  • Clear narrowly: one task, one interval, one reason
📊 Production Insight
A wide clear on a non-idempotent pipeline is how warehouses get doubled.
Clear the minimum scope, then verify row counts before and after.
Rule: narrow clear + idempotent load + count check = safe rerun.
🎯 Key Takeaway
Clearing re-queues, it doesn't delete.
Scope it tight, verify counts after, keep loads idempotent.
Clearing: The Surgical Rerun Tool Clearing: The Surgical Rerun Tool Reset state, re-queue, verify — no data deleted airflow tasks clear Resets instances to initial state Scheduler re-queues them Bookkeeping only — no data deleted Scope it narrowly One task · one interval · one instance Wide clears re-run healthy tasks Non-idempotent loads double data Narrow clear + idempotent load Verify row counts after the rerun THECODEFORGE.IO
thecodeforge.io
Airflow Backfill Catchup

4. Reruns Without Double Loads

Every rerun story ends at the same question: will running this task again produce the same data, or duplicate it?

The answer is idempotency. A load task that inserts with no key check duplicates on rerun. A load task that upserts by a natural key — or deletes the interval's partition first — reproduces the same rows.

The production pattern for date-partitioned loads: delete the partition for the data interval, then insert. Rerun the backfill as many times as you like; the data is identical.

dags/idempotent_load.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from airflow.decorators import task
from airflow.providers.postgres.hooks.postgres import PostgresHook


@task
# Idempotent: delete the interval's partition, then insert
def load_market_data(rows: list[dict], interval_end: str):
    hook = PostgresHook(postgres_conn_id="warehouse")
    with hook.get_conn() as conn:
        with conn.cursor() as cur:
            cur.execute(
                "DELETE FROM market_data WHERE interval_date = %s", (interval_end,)
            )
            for row in rows:
                cur.execute(
                    "INSERT INTO market_data (symbol, interval_date, close) VALUES (%s, %s, %s)",
                    (row["symbol"], interval_end, row["close"]),
                )
📊 Production Insight
Non-idempotent loads turn every rerun into a data-quality incident.
The delete-then-insert pattern makes reruns free.
Rule: if a task can't safely run twice, fix it before you ever backfill.
🎯 Key Takeaway
Rerun safety = idempotency, not caution.
Delete the partition, then insert. Repeat as needed.

5. Scheduled vs Manually Triggered Runs

The grid view distinguishes scheduled runs from manual runs, and the distinction matters for backfills.

Scheduled runs carry the data interval in their run metadata. Manual triggers (the Trigger DAG button, the API, the CLI) also get a data interval — the next one — which is why a manual run during an active interval can confuse people: it runs the same interval as the scheduler is about to run.

The backfill CLI creates runs with their own intervals, marked as backfill runs. When you look at the grid, backfill runs and scheduled runs are different entries with the same interval — which is exactly why duplicate data protection (idempotent loads) matters again.

💡Reading the grid
Each cell is a task instance for a specific interval and run. Backfill runs and scheduled runs for the same interval are separate cells. If the data is idempotent, that's harmless; if not, it's a duplicate.
📊 Production Insight
Teams re-run a failed day by clicking Trigger DAG, not knowing it creates a new run entry.
The run history then shows two runs for one interval — one healthy, one a clone.
Rule: use backfill or clear for reruns, not manual triggers, so the history stays readable.
🎯 Key Takeaway
Manual triggers and backfills create new run entries.
Rerun with backfill or clear — keep the run history honest.

6. The Production Backfill Checklist

A backfill is a controlled operation. Before running one on production, walk this checklist.

  1. Confirm the loads are idempotent — test the rerun on a staging interval.
  2. Set catchup=False on the DAG so the scheduler doesn't race you.
  3. Pick an explicit range, inclusive start, exclusive end.
  4. Cap concurrency: --max-active-runs 2-4 for warehouse loads.
  5. Monitor the grid view and the warehouse's load; throttle if queries queue.
  6. After completion, compare row counts against the source per interval.
  7. Document the backfill in the runbook: range, duration, row counts.
backfill_checklist.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
# Step 3-4: the disciplined backfill
airflow dags backfill market_etl \
  --start-date 2026-01-01 \
  --end-date 2026-03-31 \
  --max-active-runs 4

# Step 6: per-interval verification
SELECT interval_date, count(*) AS rows
FROM market_data
WHERE interval_date BETWEEN '2026-01-01' AND '2026-03-30'
GROUP BY interval_date
ORDER BY interval_date;
📊 Production Insight
Every skipped checklist step is a known incident pattern: un-capped runs, non-idempotent loads, unverified counts.
The checklist exists because each step was learned from a page.
Rule: no checklist, no backfill. It's that simple.
🎯 Key Takeaway
Checklist order: idempotency, catchup off, explicit range, capped runs, verify counts.
Backfill without the checklist is how the storm starts.
● Production incidentPOST-MORTEMseverity: high

The 90-Day Catchup Storm

Symptom
Within minutes of the deploy, the grid view showed 90 queued runs. The API provider throttled the client; the warehouse ran out of connection slots; tasks failed in waves and retried in waves.
Assumption
The deploy script recreated the DAG with its original start_date, assuming Airflow would only run the current interval. Nobody noticed catchup was enabled.
Root cause
Airflow's default for catchup is False in modern versions, but the re-deployed DAG file explicitly set catchup=True. With start_date 90 days in the past, the scheduler legitimately queued one run per missed data interval. Nothing limited max_active_runs, so all 90 ran at once.
Fix
The team set catchup=False as the DAG default and used airflow dags backfill with an explicit date range for the 90 missing days, with --max-active-runs limited to 4. The storm stopped; the backfill completed over a weekend at controlled concurrency.
Key lesson
  • Never deploy a DAG with catchup=True and a historical start_date.
  • Backfill is a deliberate operation with an explicit range, not a deploy side-effect.
  • max_active_runs is the shock absorber for any backfill.
  • Idempotent tasks are what make a rerun safe — catchup multiplies their importance.
Production debug guideSymptom to Action4 entries
Symptom · 01
A flood of runs appears after a deploy
Fix
Immediately check catchup in the DAG file and the UI. If catchup=True with a past start_date, set max_active_runs low, pause the DAG, and clear the unintended runs with a date range.
Symptom · 02
A backfill run fails halfway through
Fix
Backfills are per-interval runs; a failure affects one interval. Clear the failed interval's task instances (airflow tasks clear --start-date --end-date) and re-run the backfill for that range only.
Symptom · 03
Backfilled data is duplicated in the warehouse
Fix
The load task isn't idempotent. Check for upserts vs plain inserts; delete the backfill range's rows and re-run after making the load idempotent.
Symptom · 04
Backfill ignores the schedule and runs all at once
Fix
You didn't limit concurrency. Re-run with --max-active-runs=4 and a narrower --start-date/--end-date range.
★ Quick Debug Cheat Sheet — Backfill & RerunsThe exact commands to control catchup, backfill, and reruns.
Catchup storm after deploy
Immediate action
Pause the DAG and check its config
Commands
airflow dags pause <dag_id>
airflow config get-value core catchup_by_default
Fix now
Set catchup=False; clear the unintended runs with airflow tasks clear --start-date --end-date
Need to fill 90 missed intervals safely+
Immediate action
Run a limited backfill
Commands
airflow dags backfill <dag_id> --start-date 2026-01-01 --end-date 2026-03-31 --max-active-runs 4
airflow dags list-runs -d <dag_id> | grep backfill
Fix now
Keep max-active-runs small and idempotent loads on; monitor via the grid view
One failed interval in a backfill+
Immediate action
Clear and re-run just that interval
Commands
airflow tasks clear <dag_id> --start-date 2026-02-14 --end-date 2026-02-14
airflow dags backfill <dag_id> --start-date 2026-02-14 --end-date 2026-02-14
Fix now
Clear the interval's instances, then backfill the same range
Duplicated rows from a rerun+
Immediate action
Verify the load task is idempotent
Commands
SELECT count(*) FROM target WHERE load_date = '2026-02-14';
grep -n 'INSERT\|MERGE\|upsert' dags/<load_task>.py
Fix now
Switch to upsert-by-key; delete the dup range and re-run once
Backfill Mechanisms
AspectCatchup=TrueBackfill CLIManual trigger
TriggerAutomatic at deployExplicit commandUI/API button
Range controlAll missed intervalsExact datesNext interval
Concurrency capNone built in--max-active-runsDAG settings
Run history labelScheduledBackfillManual
Risk profileStorm on deployControlledClone runs
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
backfill_commands.shairflow dags backfill market_etl \2. The Backfill CLI
clear_commands.shairflow tasks clear market_etl --task-id load_market_data \3. Clearing Task Instances Safely
dagsidempotent_load.pyfrom airflow.decorators import task4. Reruns Without Double Loads
backfill_checklist.shairflow dags backfill market_etl \6. The Production Backfill Checklist

Key takeaways

1
Catchup auto-backfills every missed interval
storm or silence, pick deliberately.
2
The backfill CLI is the controlled tool
explicit range, capped concurrency, reset flag.
3
Clearing resets state, never deletes data; scope it narrow.
4
Idempotent loads are what make any rerun safe.
5
Production rule
no checklist, no backfill.

Common mistakes to avoid

4 patterns
×

Deploying with catchup=True and a historical start_date

Symptom
A flood of DAG runs queues at deploy time, hammering APIs, the DB, and the warehouse simultaneously.
Fix
Default to catchup=False in production; run explicit backfills for the intervals you actually need.
×

Backfilling without a concurrency cap

Symptom
The backfill inherits full DAG concurrency; warehouses and APIs degrade under load.
Fix
Always pass --max-active-runs (2-4 for warehouse loads) and keep the range explicit.
×

Re-running a failed day with a manual trigger

Symptom
Two run entries exist for the same interval; history becomes unreadable and data doubles if loads aren't idempotent.
Fix
Use airflow tasks clear with a narrow range, or backfill the interval — not the Trigger DAG button.
×

Backfilling a non-idempotent pipeline

Symptom
Row counts double for backfilled intervals; dedupe runs become the next incident.
Fix
Make loads idempotent first (delete-partition-then-insert or upsert by key), then backfill.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is catchup in Airflow and why is it dangerous?
Q02SENIOR
Walk me through backfilling 90 days of a production DAG safely.
Q03JUNIOR
What does clearing a task instance do?
Q01 of 03SENIOR

What is catchup in Airflow and why is it dangerous?

ANSWER
Catchup determines what happens to missed data intervals between start_date and now. With catchup=True, the scheduler queues runs for every missed interval automatically. It's dangerous when combined with a historical start_date, because it floods the scheduler, APIs, and warehouse with simultaneous runs. Production practice is catchup=False plus explicit, capped backfills.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What's the default for catchup in modern Airflow?
02
Does a backfill interfere with the regular schedule?
03
Is --end-date inclusive or exclusive?
04
Why does my manual trigger create a duplicate run for today?
05
How do I verify a backfill actually loaded the right data?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

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 Dynamic DAGs
19 / 37 · Airflow
Next
Airflow Datasets and Assets