Airflow Data Quality: The Rerun That Loaded Twice!
Airflow data quality gates stop double loads: idempotent upserts, row-count checks, and dedupe gates.
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
- ✓Experience writing DAGs with load tasks.
- ✓A Postgres (or similar) data warehouse for the upsert examples.
- ✓Understanding of task dependencies and the graph view.
- Idempotency is the rerun safety net: the same DAG run can execute twice with identical results.
- Upserts by business key or partition replace append-only loads that duplicate rows.
- Quality checks are first-class tasks: row-count parity, dedupe, and null-rate gates between load and consumers.
- A manual rerun double-loaded our analytics table because no dedupe key and no gate existed.
- Place gates before consumers: a failed check must stop the DAG, not just log a warning.
- Lineage and data contracts make quality everyone's problem, not just the pipeline owner's.
Think of a delivery van that unloads boxes onto the same shelf every morning. If a driver gets a second order to make the same delivery, the shelf now holds two copies of every box. A quality gate is a stock check: it counts the boxes before unloading and after, and refuses the second delivery if the shelf already has them. Idempotency is a smarter shelf: it swaps out old boxes instead of stacking new ones.
Every Airflow load task has a rerun problem. The DAG fails, you clear it, it runs again — and if the load appends instead of upserts, the table now holds two copies of everything. This is the most common data-quality failure in production, and it's almost never caught by the DAG's own success status.
Our version: an analytics table got double-loaded by a manual rerun. Nothing failed. The DAG was green, the checks were 'fine', and the dashboards quietly showed every number twice.
The fix is boring and permanent: idempotent loads, quality checks as real tasks, and gates placed before consumers can touch the data.
A green DAG is a promise that the work ran. It says nothing about the data.
1. Idempotency: The Rerun Safety Net
Idempotency means running the same operation twice produces the same end state as running it once. For loads, that's an upsert by business key or a write into a partition boundary that gets replaced.
Airflow reruns are a fact of life: clears, backfills, catchups, manual triggers. Every one of them is safe when the load is idempotent and dangerous when it isn't.
Our load was INSERT INTO analytics.orders SELECT ... FROM staging.orders. Pure append. The second run didn't fail — it duplicated.
An append-only load is a promise that you'll never rerun. You will rerun.
2. Quality Checks as First-Class Tasks
A quality check is not a dashboard or a notebook cell. It's a task in the DAG with a dependency edge, and when it fails, the DAG fails.
Row-count parity between staging and target catches lost and duplicate batches. Null-rate checks catch broken transforms. Freshness checks catch stale partitions.
Each check is a task that raises on violation. No raise, no failure — and a check that only logs is a lie wearing a green icon.
The graph view should show it: load, then gate, then consumers. The gate is not optional; it's the reason the DAG exists.
You don't have to hand-roll every gate: the common SQL provider ships six ready-made check operators — SQLCheckOperator, SQLColumnCheckOperator, SQLTableCheckOperator, SQLIntervalCheckOperator, SQLValueCheckOperator, and SQLThresholdCheckOperator. SQLCheckOperator runs any query and fails the task when a returned value is falsy in Python — SELECT COUNT(*) FROM foo fails only when the count is 0. SQLColumnCheckOperator runs predefined column checks (null_check, min, max, distinct_check) from a column_mapping dict, with partition_clause to scope rows and tolerance for fuzzy bounds. Astronomer recommends the column and table operators over the older value and threshold variants.
- A report you read on Monday describes Friday's failure.
- A tripwire stops consumers before they touch the data.
- Placement and failure behavior are the whole design.
3. Row-Count and Dedupe Gates
Two gates catch the double-load class of bug specifically: the dedupe gate and the parity gate.
The dedupe gate runs before the load and after it: group the source by business key and fail if any key appears twice. The parity gate compares source and target counts after the load.
Neither is expensive. Both are one SQL statement each, run as tasks, and they turn the rerun failure from a silent double into a loud, green-checked DAG that stops itself.
Double-loaded analytics is exactly what these two gates exist to prevent.
SQLTableCheckOperator is the natural home for the parity rule: pass checks like {"row_count_check": {"check_statement": "COUNT(*) >= 3"}} and the operator fails when any statement evaluates false. For drift that isn't duplication — values slowly creeping out of range — SQLIntervalCheckOperator compares today's metric against the same metric from days ago, catching slow decay that a hard parity gate misses.
4. Lineage for Downstream Trust
Lineage answers the question users actually ask: where did this number come from? Airflow shows task and DAG relationships in the grid view; Airflow 3's assets make the producer-consumer links explicit.
When a downstream team sees a warning on their dashboard, lineage tells them which DAG fed it, which task wrote it, and which gate checked it. That turns 'your data is wrong' into 'the orders DAG failed the parity gate'.
Lineage is also the trust that makes quality gates acceptable to consumers: they can see the gates exist.
A table without lineage is a rumor. A table with lineage is an argument.
5. Data Contracts with Producers
A data contract is the written agreement between the producer of a table and its consumers: schema, key, freshness, volume range. When the producer breaks the contract, the pipeline should fail at ingest — not silently corrupt downstream.
A validation task at the ingest step checks the shape of the incoming batch: required columns, unique keys, null rates within bounds.
Contracts turn quality from a pipeline owner's hobby into a team standard. The producer can't change a column and hope; the contract check catches it.
No contract means every consumer re-verifies. That's the tax you stop paying.
6. The Failure Story: Double-Loaded Analytics
Monday, 09:12 — an engineer cleared the orders load because a transient DB blip failed the last task. The rerun went green. Nothing complained.
Tuesday, 10:00 — the finance dashboard showed revenue at exactly double the previous day's. The team's first instinct was a query bug. It wasn't.
The load was INSERT ... SELECT with no dedupe key. The first run had committed 48,211 rows before failing on a later task; the clear re-ran the whole DAG, and the load inserted the same 48,211 rows again.
Two facts made this inevitable: append-only load, and no gate between load and consumers. One upsert would have made the rerun harmless. One parity gate would have made it loud.
It was a Tuesday afternoon to fix. It took a Monday morning to notice.
7. The Quality-First DAG Pattern
The shape is always the same: extract, load idempotently, gate, then let consumers run. Extract and load stay atomic; gates sit between load and the rest of the world.
This pattern makes clears safe, backfills safe, and Monday-morning fire drills unnecessary. The DAG is a machine that either produces verified data or stops itself.
Ship the gates with the load — they're not a phase-two initiative. The day they ship is the day reruns stop being dangerous.
One deliberate staging exception: a soft gate. Airflow fails a run only when a leaf task ends failed, so a leaf with trigger_rule="all_done" runs regardless of upstream check results — the run stays green while on_failure_callback still fires. Use it where you want a visible warning, not a stop; production gates stay hard.
The Rerun That Loaded Twice
- A green DAG run proves the tasks executed, not that the data is correct.
- Append-only loads guarantee duplication on any rerun.
- Quality checks belong in the DAG as failing tasks, not in a notebook after the fact.
- Manual reruns are the highest-risk operation a pipeline has — make them harmless by design.
| File | Command / Code | Purpose |
|---|---|---|
| load_orders.py | UPSERT_ORDERS_SQL = """ | 1. Idempotency |
| quality_gates.py | def verify_row_parity(**context): | 2. Quality Checks as First-Class Tasks |
| dedupe_check.sql | SELECT order_id, COUNT(*) AS copies | 3. Row-Count and Dedupe Gates |
| orders_quality_gated.py | with DAG( | 7. The Quality-First DAG Pattern |
Key takeaways
Common mistakes to avoid
4 patternsAppend-only loads with no dedupe key
Quality checks that log instead of failing
Gates placed after consumers
Clearing and re-running without a procedure
Interview Questions on This Topic
What makes a load task idempotent?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
That's Airflow. Mark it forged?
4 min read · try the examples if you haven't