Airflow in Production: The Full Pipeline Case Study
Airflow in production, end to end: design doc, idempotent ETL, quality gates, alerting, CI/CD, and an incident runbook.
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
- ✓All 36 earlier articles in the Airflow series, or equivalent experience.
- ✓Working Airflow 3 environment with a Snowflake or Postgres target.
- ✓A CI system (GitHub Actions or GitLab) and an alerting channel (Slack or PagerDuty).
- Production Airflow is a platform, not a DAG folder: design docs, contracts, and runbooks ship with the code.
- The pipeline shape: idempotent extract → transform → load, quality gates as first-class tasks, and alerting on heartbeats, queue age, and SLA.
- Every load is rerun-safe: upserts keyed by natural key or partition, so clearing a task never double-loads.
- CI/CD gates every merge: parse checks, DagBag tests, and unit tests — a broken DAG never reaches the scheduler.
- The runbook is a live document: symptoms, first commands, escalation — the thing that turns incidents into post-mortems.
- DAGs reach every worker as a versioned bundle (GitDagBundle in Airflow 3) — the scheduler sends instructions, never files.
Building a pipeline for production is like opening a real kitchen, not a home stove. You need a recipe book (design doc), a dishwasher rule that no plate goes out twice (idempotency), a health inspector (quality gates), a fire alarm (alerting), a door policy for new dishes (CI/CD), and a binder that says exactly what to do when the stove catches fire (runbook). A home cook with the best stove still burns the kitchen; the pro has the whole system.
Thirty-six articles of incidents, and they all rhyme. The cron job that double-billed. The sensor that burned a worker slot. The /tmp that made reruns nondeterministic. The schema change that skipped review. Every one of those failures was preventable with the same machinery — this capstone builds all of it at once.
The setup: a mid-size retailer, 60M order rows a day, a Snowflake warehouse, and a mandate to make analytics trustworthy by morning. Two engineers, six weeks, one Airflow platform.
This is the full case study: the design doc first, then the build in phases — idempotent ETL, quality gates, alerting, CI/CD, and the incident runbook that holds it together.
The toy DAG era ends here.
If you take one thing from the whole series, let it be this: production is a system, not a script. Design it, test it, monitor it, and write down what to do when it breaks — you'll thank yourself at 2 AM.
1. The Company, the Problem, the Constraints
The company is a mid-size retailer with 60M order rows a day: web orders, store POS, returns, refunds. Analytics ran on cron jobs and ad-hoc scripts — the cron job that double-billed every customer from the series opener was not a metaphor; it was this company, three years earlier. The mandate: trustworthy analytics by 6 AM, every morning.
The constraints are what made it a real build. No downtime window — the legacy pipelines keep running until the new ones prove themselves. Two engineers, six weeks. One warehouse (Snowflake) and a dozen sources with varying reliability. And a data team that had been burned enough to demand evidence: row counts, freshness checks, and a paper trail.
That last constraint is the whole story. Reliability wasn't a tool problem; it was a system problem. The build plan was written as a design doc before the first DAG existed.
2. The Design Doc: DAGs, Schedules, Contracts
The design doc was one page of tables, not an essay. Three pipeline families: orders (core, 3 AM, SLA 5:30), returns (core, 4 AM, SLA 6:00), and partner sync (standard, 2 AM, best-effort). Each family had an owner, a freshness SLA, and a contract: schema, natural key, and what the consumers could expect at what time.
The contracts were the part the old platform never had. A data contract is a promise: orders_ods has exactly one row per order_id, is complete for the interval, and is available by the SLA time. Writing it down first turned every later quality gate into a test of the contract instead of an opinion.
DAG design followed the contracts: one DAG per family, idempotent tasks, max_active_runs=1, catchup=False. The doc also recorded the failure playbook pointers — which article of this series applied to which failure mode. The doc was the runbook's table of contents.
3. Build Phase 1: Idempotent ETL
Phase one shipped the core DAG: ingest orders, transform, load. Three tasks, every one idempotent. Ingest downloaded to object storage keyed by data_interval_end — the /tmp incident from this series was the reason for that rule. Transform read the staged object and wrote clean objects. Load was a MERGE keyed on order_id.
Idempotency was non-negotiable from day one: the rerun-that-loaded-twice incident was the failure this company could not repeat. Every load task was written so that clearing and rerunning produced exactly the same state as the original run. The proof was part of the definition of done: clear the interval, rerun, diff the row counts — they must match.
The small details carried the weight. max_active_runs=1 so overlapping runs never raced the same partition. retries=3 with exponential backoff, per the queued-task lesson. catchup=False, with backfills only through the explicit command from the runbook. None of these were defaults; all of them were decisions.
Retries have a documented precedence the team leaned on: task-level retries, then default_args, then the deployment-wide AIRFLOW__CORE__DEFAULT_TASK_RETRIES env var — set the floor once at the platform level, override per task. The docs' incremental rule also matches this build: each run processes only its data interval's records (filter by last-modified date or sequence IDs), so a failure in one interval never blocks or re-touches the others.
4. Build Phase 2: Quality Gates and Monitoring Wiring
Phase two added the gates that turned the contract into tests. After load, three check tasks: row counts within a band of the expected volume, zero duplicate natural keys, and freshness — the interval's max loaded_at within the SLA window. A gate failure fails the DAG and pages, because silent data bugs are how trust dies.
Monitoring was wired to the three primaries from the monitoring article: scheduler heartbeat, queued-task age, and SLA misses. The dead-scheduler-nine-days incident was the cautionary tale; this platform alerts on scheduler health within five minutes of it dying. Queue-age alerts catch the sensor-slot-waste pattern from the deferrable article before it costs a window.
The alerting philosophy: page on three things, log everything else. The SLA miss alert carried the contract name and the owner. Everything else — retries, warnings, backfills — went to a channel nobody had to watch.
Logs are data too. On disposable or autoscaled nodes, task logs die with the node, so the production-deployment docs point logs at a distributed filesystem (S3, GCS) or an external service (Elasticsearch, CloudWatch, Stackdriver) — this platform shipped remote logging in the same sprint as the alerts.
5. CI/CD and Rollout
Phase three made deploys boring. Every merge to main runs the pipeline from the testing article: lint, parse check via DagBag, DAG-load tests, and unit tests on the callables. The git-sync-deployed-a-broken-DAG incident was the cautionary tale — this platform deploys images built from the tested commit, never a file copy.
The gate is the point: a DAG that fails to parse is a merge-time failure, not a 2 AM outage. CI runs airflow dags list-import-errors against the image, and the build fails on any import error before anything reaches the scheduler. Rollout follows branch promotion: feature DAGs land behind tags, and core DAGs promote through staging with a canary run before prod.
Rollback is a previous image, not a mystery. Every deploy is a versioned image; reverting is redeploying the last green build. The registry keeps the last 20 so 'what was running when this data went bad' is always answerable.
Under the hood this is Airflow 3's dag-bundle mechanism: the scheduler sends instructions — 'run task X of DAG Y' — never the files, so every node must carry the bundle. GitDagBundle versioning is the docs' production pattern for that, and it's the same guarantee this team got from image-built bundles. Patch-level upgrades stay zero-downtime when the metadata schema is unchanged — test the upgrade in staging before the first live one.
6. The Incident Runbook
The runbook shipped in the same sprint as the alerting — the 9-day silent outage from the monitoring article was the reason. It's organized by symptom, not by guess: each entry has the first commands, the likely cause, and the escalation path. The quick reference at the top of this article is its heart.
The runbook is the series playbook in one place. Scheduler heartbeat down → check scheduler health and DB. Quality gate failed → read the gate log, fix the source, clear and rerun. Import errors after deploy → run the CI gate locally, fix, redeploy — never a manual copy. Duplicate rows after rerun → verify the upsert key. Backfill storm → catchup=False, targeted backfills only.
Every incident writes back into the runbook. The post-mortem ends with a runbook diff: what entry was missing, what command was wrong, what escalation was too slow. The runbook is a living artifact precisely because the first version is always incomplete.
7. From Mid-Level to Senior: The Roadmap
The senior roadmap is visible in everything this build did differently. Juniors write tasks; seniors write contracts and gates. Juniors fix the failing DAG; seniors ask which guarantee was violated and why the check didn't catch it earlier. Juniors monitor the logs; seniors monitor the three primaries and own the SLA.
The concrete moves: own a platform, not a DAG. Write the design doc and update it. Size the platform — executors, pools, parallelism — and defend the numbers. Automate the recovery so the runbook is rarely needed, then drill it anyway. Mentor the next person on the same playbook: idempotency, quality gates, alerting, CI/CD, runbook.
The interview answers from this series are the same discipline. When a senior question asks about reruns, you answer idempotency and upsert keys. When it asks about failures, you answer heartbeats and SLA misses. The series taught the incidents; the roadmap is making them your default thinking.
8. Course Wrap-Up: The Playbook, Complete
This closes the series, so the wrap-up is the whole playbook in one breath. Foundations: DAGs are code — test them, keep top-level code pure, and let the scheduler do scheduling. Authoring: connections hold credentials, XComs carry references, sensors defer. Scheduling: data intervals, catchup discipline, backfills as explicit operations.
Deployment and ops: pick the executor for the scale, operate scheduler, workers, and triggerer like infrastructure, monitor the three primaries, and tune parsing before buying machines. Security and quality: least privilege, secret backends, idempotent loads, gates on contracts. Advanced: deferrable operators, object storage, HITL — the Airflow 3 depth that made the platform genuinely modern.
The capstone built the system that every incident in the series was pointing at. The cron job that double-billed became the DAG with states and retries. The sensor that burned a slot became the deferrable wait. The /tmp that broke reruns became deterministic object keys. The schema change that skipped review became the HITL gate.
Now go build it. The incidents are the syllabus, the playbook is the graduation — and the warehouse is waiting for its 6 AM data.
One more docs rule to carry forward: XCom is for small messages, remote storage for large payloads — push the S3 path, not the data — and when XCom traffic gets heavy, a custom XCom backend keeps the metadata DB from becoming the bottleneck.
The Full Pipeline That Finally Shipped
- Airflow doesn't fix unreviewable pipelines — it gives you the surface to build a system on.
- Idempotency is the rerun safety net: every load must be clearable and replayable.
- Alert on the three things that kill you: scheduler heartbeat, queue age, and SLA misses.
- A runbook that exists before the incident is the difference between an outage and an annoyance.
docker compose ps schedulercurl -s http://localhost:8793/health| File | Command / Code | Purpose |
|---|---|---|
| dag_manifest.yaml | pipelines: | 2. The Design Doc |
| market_etl.py | from datetime import datetime, timedelta | 3. Build Phase 1 |
| quality_gates.py | from airflow import AirflowException | 4. Build Phase 2 |
| ci_gate.yml | name: dags | 5. CI/CD and Rollout |
| runbook.md | 1. Open the gate task log: | 6. The Incident Runbook |
Key takeaways
Common mistakes to avoid
4 patternsShipping DAGs without tests
Append-only loads
No runbook before launch
Writing intermediate files to the local filesystem
Interview Questions on This Topic
Walk through how you'd design a production data platform on Airflow: DAGs, contracts, quality gates, alerting, CI/CD, and runbook.
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
That's Airflow. Mark it forged?
6 min read · try the examples if you haven't