Track main for automatic updates or a commit SHA for deterministic deployment.
Apache Airflow · Workshop
S9
DAG versioning
Bundle versions and DAG Versions are different
Git versions the whole bundle.
Airflow versions each parsed DAG separately.
Not every commit creates a new version for every DAG.
Apache Airflow · Workshop
S10
Rollback
Fix production through the same pipeline
Revert the offending commit in Git.
Run lint, import tests, and deployment again.
Inspect DagRuns created from the broken DAG Version.
Decide explicitly whether those runs should finish or stop.
A running DagRun does not switch to a corrected graph halfway through execution.
Apache Airflow · Workshop
S11
Testing strategy
Four levels catch different failures
Level
Proves
Typical tool
Import
DAG files load
DagBag
Structure
Tasks and edges exist
pytest + DAG API
Unit
Business logic is correct
plain pytest
Task execution
One task runs in Airflow context
airflow tasks test
Apache Airflow · Workshop
S12
Structure tests
Verify the graph, not only the import
deftest_dag_structure():
dag = DagBag().get_dag("order_report")
assert dag is not Noneassert len(dag.tasks) == 4
extract = dag.get_task("extract_orders")
downstream = {task.task_id for task in extract.downstream_list}
assert"clean_orders"in downstream
This test catches an accidentally removed dependency even when the DAG still imports.
Apache Airflow · Workshop
S13
Unit tests
Extract business logic from Airflow
defclean_orders_logic(orders: list) -> list:
cleaned = [order for order in orders if order > 0]
if not cleaned:
raise ValueError("No valid orders remain")
return cleaned
@taskdefclean_orders(orders: list):
return clean_orders_logic(orders)
Plain functions can be tested without Airflow, a Scheduler, or metadata database.
Apache Airflow · Workshop
Diagnostics and optimization
Apache Airflow · Workshop
S15
Task-failure checklist
Start with the cheapest evidence
Open the failed TaskInstance log.
Inspect Rendered Template for actual Jinja substitutions.
Check upstream states and distinguish failed from upstream_failed.
Import tests pass because the file is valid Python.
Template rendering fails because dss is undefined.
Rendered Template exposes the invalid expression.
Fix: {{ ds }}. Reproduce with airflow tasks test broken_template_dag print_date 2026-01-01.
Apache Airflow · Workshop
S17
Performance
Keep parsing cheap and deterministic
Do not query APIs, databases, or Variables at module level.
Use TaskGroup to organize a large graph.
Use Dynamic Task Mapping instead of hundreds of similar DAGs.
Use Pools to protect constrained external systems.
Measure Scheduler, worker, and metadata-database capacity separately.
Apache Airflow · Workshop
S18
TaskGroup
Organize one DAG without creating a sub-DAG
with TaskGroup(group_id="transform_group") as transform_group:
cleaned = clean(extract())
enrich(cleaned)
transform_group >> load()
TaskGroup changes visual organization.
It does not create another DagRun.
It replaces the removed SubDagOperator pattern.
Apache Airflow · Workshop
S19
Pools
Protect a shared resource from concurrency
airflow pools set postgres_pool 5 \
"At most five production Postgres connections"@task(pool="postgres_pool")
defquery_postgres():
hook = PostgresHook(postgres_conn_id="my_postgres")
return hook.get_first("SELECT COUNT(*) FROM my_table")
TaskGroup changes presentation. A Pool changes actual concurrency for tasks assigned with pool=.
Apache Airflow · Workshop
S20
Pool exercise
Fifty tasks, five database slots
Without a Pool
.expand() creates 50 tasks.
All try to query Postgres.
The database becomes the bottleneck.
Five-slot Pool
Five tasks run.
Forty-five remain queued.
The next task starts when a slot is released.
Set the Pool to one slot and observe mapped tasks execute sequentially.
Apache Airflow · Workshop
S21
Idempotency
Retries must not duplicate data
-- Anti-pattern: every retry inserts another row.
INSERT INTO daily_totals (report_date, total) VALUES (%s, %s);
-- Idempotent: one result per logical date.
INSERT INTO daily_totals (report_date, total) VALUES (%s, %s)
ON CONFLICT (report_date)
DO UPDATE SET total = EXCLUDED.total;
Airflow provides retries and backfills; the DAG author owns idempotent side effects.
Apache Airflow · Workshop
S22
Determinism
Use logical time for logical work
# Changes on every physical retry.
report["processed_at"] = datetime.now().isoformat()
# Stable for this logical run and its retries.
report["processed_at"] = context["ds"]
Retries happen at a different wall-clock time.
Backfills run historical intervals today.
Logical date keeps output tied to the data interval.
Apache Airflow · Workshop
Monitoring, security, and scaling
Apache Airflow · Workshop
S24
Observability
One signal is never enough
Metrics through StatsD or Prometheus.
Failure and success callbacks for actionable notifications.