Apache Airflow Interview Questions: DAGs, Scheduler, Executors and Operators
Reviewed by Mark Dickie · Last updated
7 questions on this page you can answer and see marked on the spot. Go to the first one
Apache Airflow is an open-source Python platform for authoring, scheduling and monitoring data workflows as DAGs (directed acyclic graphs). Interviewers usually check whether you can model a pipeline as tasks with dependencies, explain which component decides when a task runs, and reason about retries and reruns. Expect questions on how the scheduler and executor divide the work, and on why tasks should be idempotent so a rerun does not duplicate data. The quiz below covers the whole platform, from writing DAG files to running them in production.
What does an Apache Airflow interview test?
Interviewers tend to probe the parts of Airflow that break in production, not the UI. The table lists the concepts that come up most often and the angle each one is usually asked from.
| Concept | What to know | How it is usually asked |
|---|---|---|
| DAG | A Python object that wires tasks together with >> dependencies, and must contain no cycles | Why a cycle makes the DAG fail to load |
| Scheduler | Parses DAG files on a loop and creates task instances once their upstream tasks succeed | Why top-level code that queries a database slows every scheduler loop |
| Executor | Decides where task instances run, such as LocalExecutor, CeleryExecutor or KubernetesExecutor | The trade-off between shared workers and one pod per task |
| XCom | Stores small values passed between tasks in the metadata database | Why a large DataFrame should go to object storage with only its path passed through XCom |
| Sensor | A task that waits for a condition, such as a file landing in a bucket | Reschedule mode versus poke mode, and which one holds a worker slot while waiting |
| Catchup and backfill | With catchup on, the scheduler runs every missed data interval since start_date | Why new DAGs often set catchup to False |
How do you practise Apache Airflow for an interview?
Reading the docs will not show you how a DAG behaves when a task fails halfway. These exercises do, and each one maps to a question you are likely to be asked.
- Write a three-task DAG with the TaskFlow
@taskdecorator that passes a row count from an extract task to a load task through XCom. - Set a task to retry twice with a five-minute delay, make it fail on purpose, and read the task log in the UI to see each attempt.
- Move a task that writes to local disk onto an object store, configure the credentials as a Connection, and explain where those secrets are kept.
- Backfill a past date range, then rerun one date and confirm the output matches, which tests whether your tasks are idempotent.
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 tracked 465 job postings asking for Apache Airflow in October 2026.
- Roles asking for Apache Airflow advertise a median base salary of £82,500, across 89 job postings as of October 2026.
- Tarmac last reviewed these Apache Airflow interview questions on 7 November 2026.
At a glance
| Questions | 10 shown · 99 in the bank |
|---|---|
| Difficulty | 2–4 of 5 |
| Formats | Multiple choice, Fill in the blank, Flashcard, Ordering, Multiple answer, True / false, Code output, Find the bug, Short answer |
What you'll review
- cron presets
- retries
- bitshift operators
- hooks
- task instance
- sensors
- catchup
- xcom
- idempotency
- logical date
Practice questions
Try one before you open the answer. Pick an option and press Check; it's marked on the spot.
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.
@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. The claim that it starts immediately when the DAG is unpaused is wrong because unpausing a DAG with catchup enabled backfills from start_date rather than running 'now'.
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.
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.
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.
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. The claim that all four tasks run strictly one after another 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.
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.
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.
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.
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.
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.
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 the claim that a sensor in poke mode is free of any risk of starving the pool when many run at once 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 (in mode="reschedule", the sensor frees its worker slot between checks instead of holding it the whole time). A timeout is essential so a sensor that will never be satisfied fails (and can alert) rather than hanging indefinitely (setting a timeout lets a sensor fail instead of waiting forever for a condition that never arrives). 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 (deferrable (async) sensors release the worker and resume via the triggerer, reducing slot usage further) — the most slot-efficient choice for long waits.
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.
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.
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`
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. The claim that TaskFlow return values are not passed automatically 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. The claim that make returns a coroutine 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.
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
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. The claim that ds is invalid is wrong — ds is a valid context key (and execution_date is the deprecated name); the claim that the SQL is vulnerable to injection misreads a safe parameterized query as injection; the claim that @task functions cannot accept **context 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.
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.
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
Job market
See apache-airflow salaries and hiring demand from live job postings.
The other 89 questions
This page shows 10 and marks what you pick. That's as far as a page can go. A free account opens the other 89 and keeps every answer. What you miss comes back until it's right: after a day, then at longer gaps.
Free · the whole bank · 100 answers per 30 days, each marked with written feedback