Apache Airflow Interview Questions

Reviewed by Mark Dickie · Last updated

Apache Airflow is an open-source platform for authoring, scheduling, and monitoring data pipelines as directed acyclic graphs (DAGs) of tasks. For an interview, you need a firm grasp of how DAGs are defined and parsed, the difference between operators and tasks, how the scheduler picks up work, and how executors decide where that work runs. Expect questions on XCom data passing, catchup and backfill behavior, retry and SLA configuration, and the task state machine that governs what happens when a run fails or is cleared.

ConceptWhat to knowCommon interview angle
DAG definitionPython file in dags/ parsed by the scheduler; schedule accepts cron, timedelta, or preset strings"What happens when the scheduler reads a DAG file?"
Operators vs. tasksAn operator is a template; a task is a specific instance of an operator inside a DAG"Explain the difference between an operator and a task."
XComSmall data passed between tasks via key/value pairs stored in the metadata DB"How do tasks share data, and what are the size limits?"
ExecutorsLocalExecutor for single-machine parallelism, CeleryExecutor for distributed workers, KubernetesExecutor for per-task pods"Which executor would you choose and why?"
Catchup / backfillcatchup=True runs all missed intervals since start_date; backfill targets a specific date range manually"What's the difference between catchup and backfill?"

How does the Airflow scheduler work?

The scheduler is a standalone process that continuously parses DAG files, checks which task instances are ready to run based on their dependencies and schedule interval, and pushes those tasks to the executor. Each DAG file gets its own subprocess, and the scheduler periodically refreshes them to pick up changes without a full restart.

  1. The scheduler parses every file in the dags/ folder at a configurable interval (default ~30 seconds).
  2. For each DAG, it determines which logical dates have not yet been processed and creates DagRun records accordingly.
  3. For each DagRun, it walks the task dependency graph and marks tasks whose upstream dependencies are met as scheduled.
  4. The executor pulls scheduled tasks and runs them on the configured backend (local processes, Celery workers, or Kubernetes pods).
  5. Task state transitions are written back to the metadata database, which the web UI reads to show progress.

What task states should you know for an interview?

Interviewers often ask you to trace a task through its lifecycle, especially when something fails. The key states to name without hesitation are queued, running, success, failed, upstream_failed, skipped, and up_for_retry. If a task hits its retries limit, it moves to failed; if an upstream task fails, downstream tasks with non-dummy trigger rules become upstream_failed without ever running. Clearing a task resets it to None and lets the scheduler re-queue it, which is the standard way to rerun work after a fix.

What do interviewers ask about XCom?

XCom (cross-communication) lets one task push a value and another pull it by key and task ID. The data lives in the metadata database, so it is meant for small payloads like strings, integers, or short lists, not large datasets. In Airflow 2.x, the XComBackend is pluggable, so teams can route XCom data to S3, GCS, or another object store to avoid bloating the database. A frequent follow-up asks you to compare XCom with TaskFlow decorators, which handle push and pull implicitly through Python function arguments and return values.

Key facts

  • Tarmac has 99 Apache Airflow interview questions on this topic, 10 of them on this page, at difficulty 2–4 of 5.
  • Tarmac last reviewed these Apache Airflow interview questions on 23 August 2026.

At a glance

Questions10 shown · 99 in the bank
Difficulty2–4 of 5
FormatsMultiple choice, Fill in the blank, Flashcard, Ordering, Multiple answer, True / false, Code output, Find the bug, Short answer

What you'll review

  1. cron presets
  2. retries
  3. bitshift operators
  4. hooks
  5. task instance
  6. sensors
  7. catchup
  8. xcom
  9. idempotency
  10. logical date

Practice questions

Apache Airflow/scheduling/cron-presets

A DAG is defined with schedule="@daily" and start_date=datetime(2026, 1, 1). At what wall-clock moment does the first scheduled run actually start, and which data interval does it cover?#

Options

Show answer

@daily means 0 0 * * *. Airflow runs a schedule only after its data interval has closed, so the first run — covering 2026-01-01 00:00 to 2026-01-02 00:00 — does not fire until just after midnight on 2026-01-02. Its logical_date is the interval start (2026-01-01), the day the data belongs to. New users expect midnight on the start_date itself; the 'run after the interval ends' rule is why that intuition is wrong.

Why:

@daily is a cron preset equivalent to 0 0 * * * (midnight every day). Airflow schedules a run at the end of each data interval, not the start: the run whose interval is 2026-01-01 00:00 → 2026-01-02 00:00 is only triggered once that interval has closed, i.e. just after midnight on 2026-01-02. This 'run after the interval ends' model is the single most common source of confusion for newcomers, who expect a @daily job to fire at the start of the day. The logical_date (formerly execution_date) of that first run is the interval start, 2026-01-01, which is why the templated {{ ds }} reads as the day the data belongs to rather than the day the task ran. Option c is wrong because unpausing a DAG with catchup enabled backfills from start_date rather than running 'now'.

Apache Airflow/reliability/retries

To make a task automatically re-run up to three times after a failure, you set the _____ argument to 3. To wait a fixed gap between those attempts, you set retry_delay; to make that gap grow after each failure instead of staying constant, you enable _____=True.#

Show answer

To make a task automatically re-run up to three times after a failure, you set the **retries** argument to 3. To wait a fixed gap between those attempts, you set retry_delay; to make that gap grow after each failure instead of staying constant, you enable **retry_exponential_backoff**=True.

Why:

retries sets how many times Airflow re-attempts a task instance after it fails before marking it failed for good — a per-task (or default_args) integer, so retries=3 allows three additional attempts. retry_delay (a timedelta) is the wait between attempts. By default that delay is constant; setting retry_exponential_backoff=True makes the wait grow after each failure (roughly doubling, capped by max_retry_delay), which is the standard pattern for transient failures against rate-limited or temporarily-overloaded external systems — backing off avoids hammering a struggling dependency. Retries only help when tasks are idempotent: a re-run must be safe to repeat, otherwise automatic retries can compound the damage of a partial first attempt.

Apache Airflow/dependencies/bitshift-operators

Given tasks a, b, c, you write a >> [b, c] >> d. What dependency graph does this build?#

Options

Show answer

a >> [b, c] >> d builds a diamond. >> means 'set downstream', and a list fans the edge out then in: a runs first, then b and c become eligible together (they can run in parallel), and d waits for both to finish — under the default all_success rule, both must succeed. It is sugar over repeated set_downstream calls, the idiomatic way to write fan-out/fan-in.

Why:

The >> (right-bitshift) operator is overloaded in Airflow to mean 'set downstream'. Using a list on either side fans the dependency out or in: a >> [b, c] makes both b and c downstream of a, so they become eligible to run concurrently once a succeeds; [b, c] >> d makes d downstream of both, so by the default all_success trigger rule d waits for both to complete successfully. The result is a diamond. Option b describes a linear chain (a >> b >> c >> d), which is a different graph. The list syntax is purely sugar over repeated set_downstream calls and is the idiomatic way to express fan-out/fan-in without writing each edge by hand.

Apache Airflow/data-state/hooks

What is a Hook in Airflow, and how does it relate to Connections and Operators?#

Show answer

A Hook is a reusable interface to an external system — a database, cloud service, API, or message queue (e.g. PostgresHook, S3Hook, HttpHook). It encapsulates the boilerplate of authenticating and talking to that system: opening clients, running queries, uploading files. Crucially, a Hook does not store credentials itself — it looks them up from a Connection (referenced by conn_id), which Airflow keeps in its metadata database (with secrets ideally backed by a secrets backend). This keeps credentials out of DAG code. Operators are the task-level building blocks you place in a DAG; many operators use Hooks under the hood to do their actual work (for instance, a transfer operator may use two Hooks). When you need custom logic that no existing operator covers, you typically call a Hook directly inside a @task/PythonOperator function, getting the connection management for free while writing your own flow.

Why:

The clean mental model is a three-layer separation: Connections store the credentials/endpoint (in the metadata DB, keyed by conn_id), Hooks are the reusable client that reads a Connection and talks to the external system, and Operators are the DAG-level task units that often delegate to Hooks. The interview-critical detail is that Hooks pull credentials from Connections rather than hard-coding them, which is how Airflow keeps secrets out of DAG source. Calling a Hook directly inside a custom task is the idiomatic escape hatch when no off-the-shelf operator fits.

Apache Airflow/core-concepts/task-instance

Order these stages of a single task instance's lifecycle, from the moment its DAG run begins to successful completion, top to bottom.#

Put these in order

Show answer

A task instance progresses: no_status (waiting on upstream deps) → scheduled (the scheduler judged deps met and slots available) → queued (the executor accepted it onto its work queue) → running (a worker is executing it) → success (clean exit, unblocking downstream). The scheduler owns the move into scheduled; the executor and workers own queued and running. A task stuck in scheduled means exhausted pool/concurrency; stuck in queued points at the executor or missing workers.

Why:

A task instance moves through distinct states driven by the separation between the scheduler and the executor. It starts with no status while waiting on upstream dependencies; once the scheduler sees those dependencies satisfied (and pool/concurrency limits allow), it marks the instance scheduled. The scheduler then hands it to the executor, which queues it; a worker eventually picks it up and the instance goes running; on a clean exit it becomes success, which is what unblocks downstream tasks. Understanding this pipeline matters operationally: a task stuck in scheduled usually points to exhausted pool slots or max_active_tasks, while one stuck in queued points to the executor or workers (e.g. no Celery workers available, or a Kubernetes pod that can't be scheduled). Knowing which component owns which state is how you debug a 'why isn't my task running' incident.

Apache Airflow/operators-sensors/sensors

A sensor waits for an external condition (e.g. a file landing in S3) before downstream work proceeds. Which statements about sensors are correct? Select all that apply.#

Options

Pick every one that applies.

Show answer

In the default poke mode a sensor holds a worker slot for its whole wait, so many at once can starve the pool — meaning there is a starvation risk. reschedule mode frees the slot between checks; deferrable (async) sensors go further by handing the wait to the triggerer and resuming when it fires. A timeout is essential so a never-satisfied sensor fails and alerts instead of hanging forever.

Why:

Sensors poll for a condition and have two classic modes. In poke mode (the default) the sensor sits in a running task slot for its entire wait, re-checking on poke_interval — simple, but many concurrent poke sensors can exhaust worker slots and deadlock the pool, so option d is false. In reschedule mode the sensor releases its slot between checks and is re-queued each interval, trading a little scheduling overhead for far better slot economy at scale (a). A timeout is essential so a sensor that will never be satisfied fails (and can alert) rather than hanging indefinitely (c). The modern option is deferrable sensors: they hand off the wait to the lightweight triggerer process via asyncio, freeing the worker entirely and resuming only when the trigger fires (e) — the most slot-efficient choice for long waits.

Apache Airflow/scheduling/catchup

If a DAG has catchup=True (the default in older Airflow) and a start_date two weeks in the past, unpausing it will cause the scheduler to create and run a DAG run for every missed schedule interval between the start_date and now.#

Options

Show answer

True. With catchup=True, unpausing a DAG whose start_date is two weeks back makes the scheduler create a run for every missed schedule interval up to now (throttled by max_active_runs). It is useful for backfilling history but a common foot-gun — an old start_date can launch hundreds of runs at once. Setting catchup=False skips the missed intervals and runs only the latest going forward; deliberate replays use airflow dags backfill instead.

Why:

True. catchup controls whether the scheduler 'fills in' the schedule intervals that elapsed between start_date and the present. With catchup=True, unpausing a DAG whose start_date is two weeks back triggers a run for each missed interval (subject to max_active_runs), which is great for backfilling historical data but a notorious foot-gun: people set a months-old start_date, unpause, and accidentally launch hundreds of runs that hammer a database or external API. The defensive defaults are to set catchup=False so only the most recent interval runs going forward, and/or pin max_active_runs to throttle concurrency. Note that catchup=False does not run 'all missed intervals as one' — it simply skips them and starts from the latest completed interval. To replay a specific historical window deliberately, you use airflow dags backfill rather than relying on catchup.

Apache Airflow/data-state/xcom

Using the TaskFlow API, what value does show receive and print when this DAG run executes?#

from airflow.decorators import dag, task
from datetime import datetime

@dag(start_date=datetime(2026, 1, 1), schedule=None, catchup=False)
def pipeline():
    @task
    def make():
        return {"rows": 42}

    @task
    def show(payload):
        print(payload["rows"])

    show(make())

pipeline()

Options

Show answer
It prints `42`
Why:

The TaskFlow API (@task) makes XCom passing implicit: a task's return value is pushed to XCom, and passing make() as the argument to show() wires up the dependency and pulls that value back as the function argument at run time. So show receives the actual dict {"rows": 42} and payload["rows"] prints 42. Option c is the pre-TaskFlow mental model — with classic operators you'd push/pull XCom manually via ti.xcom_push/xcom_pull, but the decorator handles it for you. Option d misreads @task as async; it is an ordinary synchronous Python callable wrapped as an operator. The key insight is that calling a TaskFlow function in the DAG body does not execute it inline — it registers a task and returns an XComArg placeholder that resolves to the real value only when the task runs.

Apache Airflow/reliability/idempotency

This daily ETL task works on the first run but corrupts the warehouse when Airflow retries it or when the interval is backfilled. What is the root cause?#

@task
def load_daily(**context):
    ds = context["ds"]
    rows = extract_rows_for(ds)
    # append today's rows into the target table
    warehouse.execute(
        "INSERT INTO sales SELECT * FROM staging WHERE day = %s",
        (ds,),
    )

Options

Show answer

The task is not idempotent: a blind INSERT appends the same day's rows again on every retry or backfill, producing duplicates. It should delete/overwrite that partition first (or use an upsert/MERGE) so re-running the same logical_date yields the same result

Why:

The bug is a violation of idempotency, the single most important property of an Airflow task. Airflow will run a task more than once for the same logical_date — on retry after a failure, on a manual re-run, or during a backfill — and a task must produce the same end state each time. This INSERT ... SELECT blindly appends the day's rows, so the second execution duplicates them. The fix is to make the write idempotent for the partition keyed by ds: delete-then-insert that day's partition, use INSERT OVERWRITE/MERGE/upsert, or write to an ds-scoped location you replace wholesale. Option b is wrong — ds is a valid context key (and execution_date is the deprecated name); option c misreads a safe parameterized query as injection; option d is wrong because **context is the standard way a task receives the runtime context. Designing every task to be safely re-runnable is what makes retries and backfills trustworthy.

Apache Airflow/scheduling/logical-date

Explain the difference between a DAG run's logical_date (formerly execution_date) and the wall-clock time the task actually runs, and why building tasks around logical_date matters.#

Show answer

The logical_date (renamed from execution_date) is the timestamp identifying the data interval a DAG run is responsible for — conventionally the start of that interval — not the moment the task executes. Airflow schedules a run only after its data interval has closed, so a @daily run for 2026-03-14 fires just after midnight on 2026-03-15: the wall-clock run time is a day later than the logical_date. Tasks should key their work off logical_date (via macros like {{ ds }} or {{ data_interval_start }}/{{ data_interval_end }}) rather than the real current time, because that is what makes them idempotent and reproducible: a retry, a manual re-run, or a backfill of the same logical_date will query the same window and produce the same result. Relying on datetime.now() instead would make every re-run read a different window, breaking backfills and retries.

Why:

logical_date marks the data interval a run owns (its start), while the task may execute much later — typically after the interval closes. The interview point is that tasks should be written against logical_date ({{ ds }}, {{ data_interval_start }}) instead of datetime.now(), because that decouples what data a run processes from when it happens to run. That decoupling is precisely what lets retries, manual re-runs, and backfills all process the identical window and stay idempotent. A strong answer connects logical_date to reproducibility/backfill safety; a weak one just defines the term without explaining why now() is dangerous.

Related interview questions

The other 89 questions

This page shows 10. A free account opens the rest and marks your answers, which a page of answers can't do. Miss one and it returns a few days later, then a week after that, until you stop missing it.

Start free

Free · the whole bank · 100 graded answers per 30 days · written feedback and job-ad quizzes on the paid plan

What moved, monthly

One email a month when the bulletin comes out: what moved in the markets we track, and the new question topics we published. Confirm your address to join. Unsubscribe any time.