◐ ai-teacher

🔀 Pipelines & Orchestration · Chapter 1

Airflow: DAGs, tasks and the backfill that must be safe

An orchestrator's job is not running things. It is running things again — after a failure, out of order, six months late — and getting the same answer.

Study this lesson free in the app →

The basics

Apache Airflow is an orchestrator: it runs your pipeline's steps in the right order, on a schedule, retries what fails, and shows you what happened. You describe the pipeline in Python as a DAG.

The problem it solves: a cron entry can start a script at 02:00. It can't say "load only after extract succeeded", retry a 503 three times, or re-run 13 March next week and get exactly the same answer. The hard part of scheduling isn't running things — it's running them again, safely.

The mental model: every run is stamped with the day it is for, not the day it happens to execute. A task asks "which day am I processing?", never "what time is it?". Combine that with writes that replace one partition, and re-runs become harmless.

Words you must know 5

DAG
directed acyclic graph: tasks plus which must finish before which (extract >> load).
Task
one unit of work, an instance of an operator.
Logical date / data interval
the window a run is responsible for ({{ ds }}, data_interval_start). A daily run for 13 March executes just after the 13th ends.
Idempotent
running it twice gives the same result as once. Delete-then-insert for one partition is the standard trick.
Backfill / catchup
running past intervals. Safe only if tasks are idempotent.

Airflow orchestrates; heavy data processing belongs in Spark, the warehouse or another engine it triggers.

Explained

Anyone can write a script and a cron entry.

The question that separates a data engineer from someone with a cron entry is: what happens when it fails at 3am and you re-run it at 9? Twice the rows? Missing day? Or exactly right?

A DAG is a directed acyclic graph: tasks, and which must finish before which. Airflow's job is to work out what can run now, run it, retry what fails, and tell you.

The critical idea is the run date. Every run is stamped with the logical interval it is for — not the wall clock it happens to execute on. A task should ask "which day am I processing?" and never "what time is it?"

That single discipline is what makes a backfill possible.

The pieces

DAG — the pipeline definition. Python, evaluated on a schedule by the scheduler.

Task — one unit of work. An operator instance.

Logical date / data interval — the window this run is responsible for. Passed in as {{ ds }} or data_interval_start.

Sensor — a task that waits for something (a file, a partition, another DAG). Use reschedule mode, not poke, or it holds a worker slot for hours.

Pool / concurrency — the guardrail that stops your DAG taking every worker and starving everything else.

XCom — small values passed between tasks. Small. It goes through the metadata database, so it is not for data.

The shape that survives a re-run

@dag(schedule='@daily', start_date=datetime(2026, 1, 1), catchup=True,
     default_args={'retries': 3, 'retry_delay': timedelta(minutes=5)})
def rides_daily():

    @task
    def load(data_interval_start=None):
        day = data_interval_start.date()          # NOT date.today()
        rows = extract(day)
        # delete-then-insert for THIS partition only — the whole trick
        warehouse.execute('DELETE FROM rides WHERE ride_date = %s', [day])
        warehouse.insert('rides', rows)
        return len(rows)

    load()

Re-run it a hundred times for the same day and the table is identical every time. That property has a name — idempotency — and without it a backfill is a data corruption event with a progress bar.

14 March, 02:10. The daily load fails — the source API returned 503.

Airflow retries at 02:15, 02:20, 02:25. All fail. The API is down for maintenance until 06:00.

At 09:00 you clear the task. Airflow re-runs it with the same logical date — 13 March, not 14 March. It deletes 13 March's partition, reloads it, and the numbers are right.

Now imagine the task had used date.today(). At 09:00 it would have loaded 14 March's data into a run meant for the 13th, left the 13th empty, and nobody would notice for a week.

At 4,000 DAGs the failure modes change.

The scheduler becomes the bottleneck before the workers do. DAG files are parsed repeatedly — top-level code runs on every parse, so an API call at the top of a DAG file is an API call every thirty seconds, forever.

catchup=True on a DAG with a 2019 start date queues six years of runs the moment you deploy it. This takes production down and it happens regularly.

Sensors in poke mode each hold a worker slot. A hundred waiting sensors is a hundred workers doing nothing.

At that size people move the heavy work off the orchestrator entirely — Airflow submits a Spark job and watches, rather than processing anything itself.

date.today() anywhere in a task. The single most common Airflow bug. It makes backfills silently wrong rather than loudly broken.

Top-level code in a DAG file. It runs on every parse. Database connections and API calls belong inside tasks.

catchup=True without thinking. Check the start date before you deploy.

Passing DataFrames through XCom. It serialises into the metadata DB. Pass a path, not the data.

Retries on a non-idempotent task. You have automated the duplication.

An orchestrator's real job is the re-run.

Every task takes its date from the run, never from the clock. Every write is delete-then-insert for that partition only.

If you cannot re-run yesterday safely, you do not have a pipeline. You have a script that worked once.

Words to know

DAGDirected acyclic graph — the tasks and their dependencies.
Logical dateThe interval a run is responsible for, independent of when it actually executes.
CatchupWhether Airflow queues all the runs between start_date and now when a DAG is deployed.
SensorA task that waits for a condition. Use reschedule mode so it releases its worker slot.
XComSmall values passed between tasks via the metadata database. Not for data.
BackfillRe-running past intervals. Safe only if tasks are idempotent.

In two minutes

Test yourself

1. A task uses date.today() to pick its partition. What breaks?

  1. Nothing, it is equivalent
  2. Backfills and retries write to the wrong day
  3. It runs slower
  4. Airflow refuses to schedule it
Show the answer

Backfills and retries write to the wrong day — A retry at 09:00 the next morning would process today rather than the interval the run is for — silently wrong data.

2. Why prefer reschedule mode over poke for a long-waiting sensor?

  1. It is faster
  2. Poke holds a worker slot for the whole wait
  3. Reschedule retries more often
  4. Poke cannot time out
Show the answer

Poke holds a worker slot for the whole wait — Poke occupies a worker while sleeping. A hundred sensors then starve the pool.

3. What makes a backfill safe?

  1. Retries
  2. Idempotent tasks that write by partition
  3. A bigger cluster
  4. Turning off catchup
Show the answer

Idempotent tasks that write by partition — Re-running must produce the same result. Delete-then-insert for the run's own partition is the usual way to get it.

4. A @daily DAG has a run with logical date 2026-03-13. When does that run actually start?

  1. At 00:00 on 13 March
  2. At 00:00 on 14 March — after the interval it covers has ended
  3. Whenever the DAG file is next parsed
  4. At the time the DAG was deployed
Show the answer

At 00:00 on 14 March — after the interval it covers has ended — A run processes a completed interval, so it fires at the end of that interval. This trips everyone the first time: the run 'for' the 13th happens on the 14th.

5. What does extract >> transform >> load declare?

  1. Three DAGs that run in parallel
  2. Data is streamed from extract to load
  3. transform runs only after extract succeeds, and load only after transform succeeds
  4. load runs first
Show the answer

transform runs only after extract succeeds, and load only after transform succeeds — >> sets upstream/downstream dependencies between tasks. No data moves through it — data goes through storage, with at most a small XCom value like a path.

6. You need to backfill 180 days without hammering the source database. Which setting limits how many runs of this DAG execute at once?

  1. max_active_runs
  2. retries
  3. catchup
  4. depends_on_past
Show the answer

max_active_runs — max_active_runs caps concurrent DAG runs, so 180 days become, say, 4 at a time. retries handles failure; catchup decides whether past runs are queued at all; depends_on_past makes each run wait for the previous run of the same task.

There are 9 questions, 3 puzzles and 5 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →