Airflow Interview Questions for Data Engineers

Airflow interviews probe the behaviour that surprises teams in production. Five questions are answered by running a real Airflow 3 pipeline on household power data, including a validation failure that Airflow marked as a successful run, an empty data interval, and the documented fix for both.

GloryTecks Team

Training & Placement Faculty

15 February 2026 7 min read

Last updated: 9 October 2026

Share:

Airflow interview questions for data engineers go beyond "what is a DAG?". Interviewers ask about the behaviour that surprises teams in production: which date a run is for, what happens downstream of a failure, and how Airflow decides whether a run succeeded. The answers below come from running a real Airflow 3 pipeline on one-minute household electricity readings (UCI: household power consumption). It ran once for a normal day, once for 20 March 2010, when 84% of the readings are missing, and once as a rerun. One of the results is a failure that Airflow reported as a success.

python
import glob
import os
from datetime import datetime, timedelta, timezone

import pandas as pd
from airflow.exceptions import AirflowFailException
from airflow.sdk import dag, task

LANDING, WAREHOUSE = "landing", "warehouse"                     # one CSV per day arrives in landing/


def build(dag_id, fixed):
    def on_failure(context):                                     # the fixed version alerts from a callback
        open("alerts.log", "a").write(f"{dag_id} failed\n")

    @dag(dag_id=dag_id, schedule="@daily", start_date=datetime(2010, 3, 1, tzinfo=timezone.utc), catchup=False,
         default_args={"retries": 2, "retry_delay": timedelta(minutes=5)}, on_failure_callback=on_failure if fixed else None)
    def power_daily():
        @task
        def extract(logical_date=None, data_interval_start=None, data_interval_end=None):
            day = logical_date.strftime("%Y-%m-%d")              # the date this run is for
            rows = pd.read_csv(f"{LANDING}/{day}.csv")
            with open(f"interval_{dag_id}_{day}.txt", "w") as f:
                f.write(f"{data_interval_start:%Y-%m-%d %H:%M} -> {data_interval_end:%Y-%m-%d %H:%M}")
            return {"day": day, "rows": len(rows), "missing": int(rows.Global_active_power.isna().sum())}

        @task
        def validate(batch):
            if batch["missing"] > 0.1 * batch["rows"]:           # retrying will not bring readings back
                raise AirflowFailException(f"{batch['day']}: {batch['missing']} of {batch['rows']} readings missing")
            return batch

        @task
        def load(batch):
            out = f"{WAREHOUSE}/day={batch['day']}"
            os.makedirs(out, exist_ok=True)
            pd.read_csv(f"{LANDING}/{batch['day']}.csv").to_csv(f"{out}/part.csv", index=False)   # overwrite: reruns are safe
            return batch["day"]

        @task
        def meter_total(day, meter):                             # one mapped task instance per sub-meter
            return float(pd.read_csv(f"{WAREHOUSE}/day={day}/part.csv")[f"Sub_metering_{meter}"].sum())

        @task(trigger_rule="all_done")
        def cleanup():
            for f in glob.glob("tmp_*"):
                os.remove(f)

        checked = validate(extract())
        totals = meter_total.partial(day=load(checked)).expand(meter=[1, 2, 3])
        if fixed:
            totals >> cleanup().as_teardown()                    # teardowns do not decide the run's state
        else:
            @task(trigger_rule="one_failed")
            def alert():
                open("alerts.log", "a").write(f"{dag_id} alert task ran\n")
            [checked, totals] >> alert()
            totals >> cleanup()

    return power_daily()


naive, fixed = build("power_daily", fixed=False), build("power_daily_fixed", fixed=True)

if __name__ == "__main__":
    raw = pd.read_csv("household_power_consumption.txt", sep=";", na_values="?")   # UCI household power
    raw["day"] = pd.to_datetime(raw.Date, format="%d/%m/%Y").dt.strftime("%Y-%m-%d")
    os.makedirs(LANDING, exist_ok=True)
    for d in ("2010-03-19", "2010-03-20"):
        raw[raw.day == d].drop(columns="day").to_csv(f"{LANDING}/{d}.csv", index=False)
    for pipeline, d in ((naive, "2010-03-19"), (naive, "2010-03-20"), (naive, "2010-03-19"), (fixed, "2010-03-20")):
        run = pipeline.test(logical_date=datetime.fromisoformat(d).replace(tzinfo=timezone.utc))
        states = {(ti.task_id if ti.map_index < 0 else f"{ti.task_id}[{ti.map_index}]"): ti.state for ti in run.get_task_instances()}
        rows = sum(len(pd.read_csv(f)) for f in glob.glob(f"{WAREHOUSE}/day=*/part.csv"))
        print(f"{pipeline.dag_id} {d}: run {run.state} | data interval {open(f'interval_{pipeline.dag_id}_{d}.txt').read()} | warehouse rows {rows:,}")
        print("   ", ", ".join(f"{k}={v}" for k, v in sorted(states.items())))
    print("alerts.log:", open("alerts.log").read().strip().replace("\n", " | "))
text
power_daily 2010-03-19: run success | data interval 2010-03-19 00:00 -> 2010-03-19 00:00 | warehouse rows 1,440
    alert=skipped, cleanup=success, extract=success, load=success, meter_total[0]=success, meter_total[1]=success, meter_total[2]=success, validate=success
power_daily 2010-03-20: run success | data interval 2010-03-20 00:00 -> 2010-03-20 00:00 | warehouse rows 1,440
    alert=success, cleanup=success, extract=success, load=upstream_failed, meter_total[0]=upstream_failed, meter_total[1]=upstream_failed, meter_total[2]=upstream_failed, validate=failed
power_daily 2010-03-19: run success | data interval 2010-03-19 00:00 -> 2010-03-19 00:00 | warehouse rows 1,440
    alert=skipped, cleanup=success, extract=success, load=success, meter_total[0]=success, meter_total[1]=success, meter_total[2]=success, validate=success
power_daily_fixed 2010-03-20: run failed | data interval 2010-03-20 00:00 -> 2010-03-20 00:00 | warehouse rows 1,440
    cleanup=success, extract=success, load=upstream_failed, meter_total[0]=upstream_failed, meter_total[1]=upstream_failed, meter_total[2]=upstream_failed, validate=failed
alerts.log: power_daily alert task ran | power_daily_fixed failed

A1. What is the difference between the logical date and the data interval?

The logical date identifies the run; the data interval is the period of data it should process. For a classic daily schedule, a run covers midnight to midnight and starts after the interval ends. But every run here reported an empty interval, start equal to end, such as 19 March 00:00 to 19 March 00:00. In Airflow 3, a manual run's data interval depends on the timetable and need not match the logical date, and bare cron schedules can default to a timetable without data intervals (Airflow: upgrading to Airflow 3). The documented advice is to use logical_date explicitly when you need the run's date, which is what this DAG does.

What to say: know which dates your tasks rely on, and do not port Airflow 2 code that reads data_interval_start without checking.

A2. What happens to tasks downstream of a failure?

By default, a task runs only if all its upstream tasks succeeded. When validation failed on 20 March, load and the three mapped meter_total tasks became upstream_failed and never ran. Trigger rules change this: the alert task with one_failed ran because something failed, and cleanup with all_done ran regardless. Validation raised AirflowFailException, so the day's missing data was not retried pointlessly.

A3. How does Airflow decide whether a DAG run succeeded?

From the leaf tasks, the ones with no downstream tasks. The run succeeds if every leaf succeeded or was skipped (Airflow: Dag runs). That produced the surprise here: the 20 March run was marked success, although validation failed and nothing was loaded, because its leaves, alert and cleanup, both succeeded. Any dashboard or downstream sensor watching run states would have seen a green run, and the missing day would only have been noticed when someone looked at the numbers.

The fix that ran: make cleanup a teardown task, which is ignored when evaluating run state (Airflow: setup and teardown), and move the alert to an on_failure_callback. The fixed DAG's run for 20 March was marked failed, and the callback still wrote the alert.

A4. What is dynamic task mapping?

Creating one task instance per input at run time with expand(), while partial() sets the shared arguments (Airflow: dynamic task mapping). Here, one meter_total instance ran for each of the three sub-meters, shown as meter_total[0] to [2]. In practice the list often comes from an upstream task, for example one instance per file that arrived.

A5. How do you make reruns safe?

Make each run write a deterministic location for its date, overwriting rather than appending. The load step wrote warehouse/day=2010-03-19/part.csv and overwrote it on the rerun, so the warehouse held 1,440 rows, one per minute of that day, before and after. An appending load would have doubled them. Reruns are routine in Airflow: clearing a task, backfilling a range and retries all run tasks again.

Other questions to expect

  • What is catchup? With catchup=True, the scheduler creates runs for every interval between the start date and now that has not run yet; with False, only the latest.
  • Sensors? Tasks that wait for a condition, such as a file. Use deferrable or reschedule mode so a waiting sensor does not hold a worker slot.
  • XComs? Small values passed between tasks; pass references to data, such as paths, not the data itself.
  • Pools and concurrency? Pools cap how many tasks can use a shared resource at once, such as a database that tolerates only five connections; max_active_runs and max_active_tasks limit how much of one DAG runs in parallel, which matters most during a large backfill.

How to answer Airflow questions

  • Know which date drives each task, and say it explicitly.
  • Think about leaves when you add alerting or cleanup tasks.
  • Design every task to be rerun.

The basics are in the Airflow tutorial, ML scheduling in Airflow for ML pipelines, and tool choice in Airflow vs Dagster vs Prefect. Load design is covered in ETL interview questions.

What are common Airflow interview questions?

DAGs, tasks and operators, scheduling with logical dates and data intervals, catchup and backfill, trigger rules, retries, sensors, XComs, dynamic task mapping, pools, idempotent tasks, and how DAG run state is determined.

Why does my Airflow run show success when a task failed?

Run state comes from the leaf tasks. If the leaves are tasks that run after failures, such as cleanup or alert tasks, the run can succeed. Use teardown tasks or failure callbacks instead.

What changed about dates in Airflow 3?

Manual runs' data intervals are no longer guaranteed to match the logical date, and bare cron schedules can use a timetable without data intervals. Use logical_date explicitly when a task needs the run's date.

How do I make an Airflow task idempotent?

Have it produce the same result for the same date however many times it runs, for example by overwriting a date-partitioned output or by merging on a key.

Pipeline orchestration is part of the Data Engineering course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master Interview Questions?

Join GloryTecks’ expert-led Data Engineering training in Hyderabad with real-time projects and 100% placement support.

Explore course