Home DevOps Airflow in Production: The Full Pipeline Case Study
Advanced 6 min · August 1, 2026

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.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Production
production tested
August 01, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 15-20 min read
  • 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 Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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.
✦ Definition~90s read
What is Airflow in Production?

Airflow in production is the capstone playbook: a design-doc-first build of idempotent ETL with quality gates, heartbeat-based alerting, CI/CD deployment, and a written incident runbook — the whole series distilled into one case study.

Building a pipeline for production is like opening a real kitchen, not a home stove.
Plain-English First

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.

📊 Production Insight
Name the problem before naming the tools.
Constraints — no downtime, two engineers, six weeks.
The data team demands evidence; design for it.
🎯 Key Takeaway
Production builds start with the problem statement.
Constraints decide the architecture.
Evidence is the product; DAGs are the means.
The Build: Problem, Constraints, Mandate The Build: Problem, Constraints, Mandate Mid-size retailer · 60M order rows a day The problem 60M rows/day · cron + ad-hoc scripts The constraints No downtime · 2 engineers · 6 weeks The mandate Trustworthy analytics by 6 AM daily The evidence Row counts · freshness · paper trail A system problem, not a tool problem THECODEFORGE.IO
thecodeforge.io
Airflow In Production

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.

dag_manifest.yamlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# dag_manifest.yaml — the platform contract
pipelines:
  market_etl:
    schedule: "0 3 * * *"
    family: core
    sla: "complete by 05:30 UTC"
    natural_key: order_id
    quality_gates: [row_counts, dedupe, freshness]
    owners: [data-platform]
  returns_etl:
    schedule: "0 4 * * *"
    family: core
    sla: "complete by 06:00 UTC"
    natural_key: return_id
    quality_gates: [row_counts, dedupe]
    owners: [data-platform]
  partner_sync:
    schedule: "0 2 * * *"
    family: standard
    sla: best_effort
    quality_gates: [row_counts]
    owners: [integrations]
Output
3 pipelines · 2 core with SLA · contracts and gates declared
📊 Production Insight
One page of tables beats twenty pages of prose.
Contracts name the key, the SLA, and the guarantees.
The design doc is the runbook's table of contents.
🎯 Key Takeaway
Design doc first: families, schedules, contracts.
Contracts turn quality gates into tests.
The doc outlives the build — keep it current.
The Design Doc: One Page of Tables The Design Doc: One Page of Tables Families, schedules, SLAs, contracts core · schedule · SLA · key · gates orders — market_etl 3 AM · SLA 05:30 · key order_id gates: row_counts, dedupe, freshness returns — returns_etl 4 AM · SLA 06:00 · key return_id gates: row_counts, dedupe partner_sync (standard) 2 AM · SLA best-effort gates: row_counts One row per key, complete by SLA The design doc is the runbook's index One DAG per family · catchup=False THECODEFORGE.IO
thecodeforge.io
Airflow In Production

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.

market_etl.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from datetime import datetime, timedelta

from airflow import DAG
from airflow.decorators import task
from airflow.providers.snowflake.operators.snowflake import SnowflakeOperator

with DAG(
    dag_id="market_etl",
    schedule="0 3 * * *",
    start_date=datetime(2026, 1, 1),
    catchup=False,
    max_active_runs=1,
    tags=["core", "sla_critical"],
) as dag:

    @task(retries=3, retry_delay=timedelta(minutes=5), max_retry_delay=timedelta(hours=1))
    def ingest_orders(interval_end: str) -> str:
        src = ObjectStoragePath(f"s3://raw/orders/{interval_end}/feed.json")
        return src.as_uri()

    @task
    def transform_orders(src_uri: str) -> str:
        raw = ObjectStoragePath(src_uri)
        clean = ObjectStoragePath(f"s3://clean/orders/{src_uri.split('/')[-2]}/orders.parquet")
        clean.parent.mkdir(parents=True, exist_ok=True)
        clean.write_bytes(normalize_orders(raw.read_bytes()))
        return clean.as_uri()

    load_orders = SnowflakeOperator(
        task_id="load_orders",
        snowflake_conn_id="snowflake_prod",
        sql="""
        MERGE INTO orders_ods t
        USING (SELECT $1::varchar AS order_id, $2::number AS amount
               FROM @staging/orders/{{ data_interval_end }}/orders.parquet) s
        ON t.order_id = s.order_id
        WHEN MATCHED THEN UPDATE SET amount = s.amount, loaded_at = CURRENT_TIMESTAMP()
        WHEN NOT MATCHED THEN INSERT (order_id, amount, loaded_at)
        VALUES (s.order_id, s.amount, CURRENT_TIMESTAMP())
        """,
    )

    load_orders << transform_orders(ingest_orders("{{ data_interval_end }}"))
Output
ingest_orders → transform_orders → load_orders · MERGE upsert · rerun-safe
📊 Production Insight
Every load is an upsert keyed by natural key.
Rerun proof is part of definition of done.
Defaults are decisions, not accidents.
🎯 Key Takeaway
Idempotency is the rerun safety net.
Upsert by natural key; key by interval.
Clearing and rerunning must be boring.
Phase 1: Idempotent ETL, Rerun-Safe Phase 1: Idempotent ETL, Rerun-Safe ingest → transform → load, interval-keyed ingest_orders Object storage keyed by data_interval_end transform_orders Staged → clean objects clean parquet objects load_orders MERGE upsert keyed on order_id · rerun-safe clear + rerun → identical state Retries=3 with backoff · catchup=False Proof: rerun twice, row counts match THECODEFORGE.IO
thecodeforge.io
Airflow In Production

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.

quality_gates.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from airflow import AirflowException
from airflow.decorators import task


@task

def check_row_counts(expected: int) -> None:
    rows = run_query("SELECT count(*) FROM orders_ods WHERE loaded_date = CURRENT_DATE")
    if rows < expected * 0.99:
        raise AirflowException(f"row count {rows} below 99% of expected {expected}")


@task

def check_dedupe() -> None:
    dups = run_query(
        "SELECT count(*) FROM (SELECT order_id FROM orders_ods "
        "GROUP BY order_id HAVING count(*) > 1)"
    )
    if dups:
        raise AirflowException(f"{dups} duplicate order_ids in orders_ods")


@task

def check_freshness(sla: str) -> None:
    latest = run_query("SELECT max(loaded_at) FROM orders_ods")
    if latest > sla:
        raise AirflowException(f"max loaded_at {latest} exceeds SLA {sla}")
Output
check_row_counts ✓ · check_dedupe ✓ · check_freshness ✓ — contract holds
📊 Production Insight
Gates are contract tests, not opinions.
Page on heartbeat, queue age, and SLA misses.
Silent data bugs kill trust; gates make them loud.
🎯 Key Takeaway
Quality gates make contracts testable.
Three alert primaries; everything else logs.
Loud failure beats silent corruption.

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.

ci_gate.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# .github/workflows/dags.yml — the merge gate
name: dags
on:
  pull_request:
    paths: ["dags/**", "tests/**"]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: python -m pytest tests/ -x                # DagBag + unit tests
      - run: airflow dags list-import-errors --output table   # parse gate
      - run: docker build -t airflow-dags:${{ github.sha }} .
      - run: docker push registry.thecodeforge.io/airflow-dags:${{ github.sha }}
Output
validate: pytest ✓ · import-errors: none ✓ · image pushed and tagged
📊 Production Insight
Deploys ship the tested commit, never a file copy.
Parse safety is a hard merge gate.
Rollback is the last green image.
🎯 Key Takeaway
CI/CD gates make broken DAGs merge-time events.
Images are versioned; rollback is a redeploy.
Deploys should be boring by design.

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.

runbook.mdMARKDOWN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Runbook — market_etl

## Quality gate failed
1. Open the gate task log:
   `airflow tasks test market_etl quality_gate <date>`
2. Find the threshold breach (row count / dedupe / freshness).
3. Fix the source or the contract — never the gate to pass.
4. Clear the interval and rerun:
   `airflow tasks clear market_etl --start-date <d> --end-date <d>`

## Scheduler heartbeat alert
1. `docker compose ps scheduler`
2. `curl -s http://localhost:8793/health`
3. Restart scheduler; verify DB connectivity.

## Escalation
- 1st: on-call data engineer (5 min)
- 2nd: data platform lead (15 min)
- 3rd: warehouse DBA (30 min)
Output
Every entry: symptoms → first commands → escalation
📊 Production Insight
Runbooks ship before the incidents they document.
Entries are symptoms → commands → escalation.
Post-mortems end in a runbook diff.
🎯 Key Takeaway
The runbook is the platform's operating manual.
It lives by being written before first use.
Every incident improves it; that's the loop.

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.

📊 Production Insight
Seniors own contracts, gates, and SLAs.
They size the platform and defend the numbers.
Mentorship is the playbook, passed on.
🎯 Key Takeaway
Senior = platform ownership, not task writing.
The playbook is the mentor's syllabus.
Own the outcome, not just the DAG.

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 one-line course
Production Airflow is a system: design docs and contracts, idempotent and gated loads, three alert primaries, CI/CD parse gates, and a living runbook. Everything else in the series is detail on that frame.
📊 Production Insight
Every incident in the series maps to a control.
The playbook is the system, not the tools.
Ship the platform; the warehouse waits at 6 AM.
🎯 Key Takeaway
The capstone is the series, assembled.
Incidents become controls; controls become habits.
The playbook is complete — now it's your build.
● Production incidentPOST-MORTEMseverity: high

The Full Pipeline That Finally Shipped

Symptom
The company's analytics ran on a patchwork of cron jobs and ad-hoc scripts; a broken window went unnoticed for days and every fix was a fire drill.
Assumption
The team assumed moving the same pipelines to Airflow would fix reliability by itself — 'the tool handles it'.
Root cause
Fragmented tooling and no runbook kept pipelines fragile and unreviewable: no design contracts, non-idempotent loads, no quality gates, no alerting, no deploy pipeline, and no documented recovery path.
Fix
Followed the series playbook: a design doc with DAGs, schedules, and data contracts; idempotent upsert loads; row-count and dedupe quality gates; heartbeat and queue-age alerting; CI/CD with DAG-load tests; and a written incident runbook. The pipeline ran 60 consecutive nights without a missed SLA.
Key lesson
  • 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.
Production debug guideSymptom to Action6 entries
Symptom · 01
DAG fails at the quality gate
Fix
The gate is the tripwire, not the problem. Check the gate task's log and the source data; fix the source, then clear and rerun the interval.
Symptom · 02
Scheduler heartbeat alert fires
Fix
Check scheduler container health and metadata DB connectivity first. Heartbeat alerts mean the scheduler or DB is down, not the DAGs.
Symptom · 03
Queue depth growing at 2 AM
Fix
Find the long pole: poke-mode sensors, an undersized executor, or tasks stuck in queued. Fix the slot consumer, then let the queue drain.
Symptom · 04
Rerun double-loads rows
Fix
The load task isn't idempotent. Verify the upsert key and partition predicate, fix the SQL, then clear and rerun the load.
Symptom · 05
Broken DAG reached production
Fix
The CI gate was bypassed. Run the parse check and DagBag tests locally, fix the import, and redeploy through the pipeline — never a manual copy.
Symptom · 06
Alert fatigue
Fix
Too many alerts on too many things. Cut to the three primaries — scheduler heartbeat, queued-task age, SLA misses — page on those, log the rest.
★ Production Runbook Quick ReferenceThe first five commands when the platform misbehaves.
Scheduler heartbeat alert
Immediate action
Check scheduler health
Commands
docker compose ps scheduler
curl -s http://localhost:8793/health
Fix now
Restart the scheduler; verify metadata DB connectivity.
Quality gate failed+
Immediate action
Inspect the gate task log
Commands
airflow tasks test market_etl quality_gate 2026-07-31
airflow dags list-runs -d market_etl --state failed
Fix now
Fix the source, clear the interval, and rerun.
Deployed DAG has import errors+
Immediate action
Check parse status
Commands
airflow dags list-import-errors
python -m pytest tests/ -x
Fix now
Fix the import, push, and redeploy via CI — never a manual copy.
Backfill storm after deploy+
Immediate action
Check the catchup flag and run list
Commands
airflow dags backfill market_etl --start-date 2026-07-01 --end-date 2026-07-31 --reset-dagruns
airflow tasks clear market_etl --start-date 2026-07-31 --end-date 2026-07-31
Fix now
Keep catchup=False in prod; run targeted backfills only.
Rerun loaded duplicate rows+
Immediate action
Verify the upsert key
Commands
SELECT count(*), order_id FROM orders_ods GROUP BY order_id HAVING count(*) > 1 LIMIT 10;
airflow tasks clear market_etl load_orders --start-date 2026-07-31 --end-date 2026-07-31
Fix now
Make the load an upsert by natural key, then clear and rerun.
Before vs After: The Platform Build
StageBefore (toy)After (production)
DesignImplicit, in someone's headDesign doc: DAGs, schedules, contracts, SLA
LoadsAppend-only, broken by rerunsIdempotent upserts by natural key
QualityNone — hopeRow-count, dedupe, freshness gates as tasks
AlertingFailures discovered MondayHeartbeat, queue-age, SLA alerts to Slack
DeployManual file copyCI/CD: parse check, DagBag tests, image deploy
RecoveryTribal knowledgeWritten runbook with first commands and escalation
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
dag_manifest.yamlpipelines:2. The Design Doc
market_etl.pyfrom datetime import datetime, timedelta3. Build Phase 1
quality_gates.pyfrom airflow import AirflowException4. Build Phase 2
ci_gate.ymlname: dags5. CI/CD and Rollout
runbook.md1. Open the gate task log:6. The Incident Runbook

Key takeaways

1
Production is a system
design doc, contracts, tests, monitoring, and a runbook ship with the DAGs.
2
Idempotency is the rerun safety net; every load must be clearable and replayable.
3
Quality gates turn data bugs into failed tasks, not silent corruption.
4
Page on scheduler heartbeat, queue age, and SLA misses
everything else logs.
5
CI/CD parse gates make broken DAGs a merge-time event, not a 2 AM outage.

Common mistakes to avoid

4 patterns
×

Shipping DAGs without tests

Symptom
Broken imports reach prod and stall the scheduler
Fix
Gate merges on lint, parse checks, and DagBag tests in CI.
×

Append-only loads

Symptom
Reruns double-load rows and nobody catches it
Fix
Upsert by natural key or partition from day one.
×

No runbook before launch

Symptom
The first incident becomes a multi-day outage
Fix
Write the runbook in the same sprint as the alerting.
×

Writing intermediate files to the local filesystem

Symptom
Tasks fail sporadically on Celery or Kubernetes when the next task runs on a node that never saw the file
Fix
Stage files in shared object storage (S3/GCS) and push paths through XCom — never assume two tasks share a disk.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Walk through how you'd design a production data platform on Airflow: DAG...
Q02SENIOR
A rerun of last night's load double-loaded the warehouse. Walk the incid...
Q03SENIOR
How do you make a task idempotent, and how do you prove it?
Q01 of 03SENIOR

Walk through how you'd design a production data platform on Airflow: DAGs, contracts, quality gates, alerting, CI/CD, and runbook.

ANSWER
Start with a design doc, not code: pipeline families, schedules, SLAs, and data contracts naming natural keys. Build idempotent ETL — object-storage staging keyed by interval, upsert loads by natural key — so every task is clearable and replayable. Add quality gates as tasks: row counts, dedupe, freshness. Wire the three alert primaries: scheduler heartbeat, queued-task age, SLA misses. Deploy through CI/CD with a parse gate and DagBag tests, images not file copies. Ship the runbook with first commands and escalation before the first incident.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
How long does it take to stand up a production Airflow platform?
02
What should be in the design doc before writing DAGs?
03
How do I know my load is actually idempotent?
04
How do DAGs reach every worker in a distributed deployment?
05
What's the biggest difference between mid-level and senior Airflow work?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Verified
production tested
August 01, 2026
last updated
1,750
articles · all by Naren
🔥

That's Airflow. Mark it forged?

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

Previous
Airflow vs Prefect vs Dagster
37 / 37 · Airflow