DAGs • Operators & Sensors • Scheduling & Backfill • Executors • Dynamic DAGs • 2026

Apache Airflow Interview Questions

30 questions What each one tests, an answer frame, a spoken answer 35 min read

This page is for data engineers, analytics engineers and platform engineers facing an Airflow round. Most interviews start with DAGs, operators, sensors and hooks, then test how scheduling really works: data intervals, catchup, backfills and cross-DAG dependencies. Next come XCom, templating, retries, timeouts and alerts, then executors, pools and why tasks get stuck. Senior rounds add dynamic DAGs, a failed-midnight-run scenario and stories from real production work. Each question shows what the interviewer is checking, the shape of a strong answer and a short answer to say out loud. Airflow changes between major versions, so check details against the version you use.

Search all questions by round, difficulty and level, or save the ones you want to practise.

DAG Basics 6 questions

Easy Technical round Fresher Practice question

1. What is a DAG in Airflow, and why does it have to be acyclic?

What the interviewer is really testing:
Whether you understand the core object you'll work with every day, and why the no-loops rule matters for scheduling.
Answer frame:

Definition: a directed acyclic graph of tasks, defined in a Python file, with a schedule and default settings.

Directed: edges say which task must finish before the next one can start.

Acyclic: no loops, so the scheduler can always work out a valid run order and know when a run is finished.

Sample spoken answer:

"A DAG is a directed acyclic graph, and in Airflow it's the workflow itself: a Python file that declares a set of tasks, how they depend on each other, when the whole thing should run, and shared settings like retries. Directed means each edge has a direction, so task B only starts once task A has done its part. Acyclic means you can never follow the arrows and end up back where you started. That rule is what makes scheduling possible. If extract depended on load and load depended on extract, the scheduler would have no task it could start first, and a run could never finish. Airflow checks for cycles when it parses the file and refuses a DAG that has one. If I genuinely need to repeat something, I rerun the DAG on its next schedule or loop inside a single task, not across tasks."

Red flag to avoid:

Describing a DAG as a script that runs top to bottom, or not being able to say what goes wrong if there's a cycle.

They may ask next:
  • Is a DAG the same thing as a DAG run? How are they different?
  • What happens if two DAG files declare the same dag_id?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

2. Walk me through the main parts of an Airflow deployment and what each one does.

What the interviewer is really testing:
Whether you know where things happen, which you need before you can debug a stuck task or a missing DAG.
Answer frame:

Scheduler: decides which DAG runs and task instances are due and hands ready tasks to the executor.

DAG parsing: reads the DAG files and stores their structure; newer versions run it as its own DAG processor.

Metadata database: the source of truth for runs, task states, connections and variables.

Executor and workers: the executor decides how tasks run, workers actually run them; the web UI shows it all.

Sample spoken answer:

"There are a few moving parts. The DAG files are parsed on a loop, either by the scheduler or, in newer versions, by a separate DAG processor, and the parsed structure goes into the metadata database. The scheduler reads that, creates DAG runs when they're due, and works out which task instances have their dependencies met. It hands those to the executor, which decides how they run: as local processes, through a queue to Celery workers, or as pods on Kubernetes. Workers run the task code and report the state back; in Airflow 3 they do that through an API server rather than writing to the database directly. The metadata database is the heart of it, holding every run, task state, connection and variable. The web UI reads the same database so I can see runs, logs and clear tasks. If I use deferrable operators there's also a triggerer, which waits on many async events cheaply."

Red flag to avoid:

Mixing up the scheduler and the executor, or saying the scheduler runs the task code itself.

They may ask next:
  • If the web UI is down, do scheduled tasks still run?
  • Which component would you look at first if a new DAG file never shows up in the UI?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

3. Why is it a bad idea to query a database or call an API at the top level of a DAG file?

What the interviewer is really testing:
Whether you know that DAG files are parsed again and again, and what that does to performance and to the systems you call.
Answer frame:

Parsing loop: DAG files are re-parsed every short interval, not once per run.

Cost: top-level calls run on every parse, slow parsing down and can hit the parse timeout.

Fix: keep top-level code to building the graph; do real work inside tasks and use templates for variables.

Sample spoken answer:

"Airflow doesn't read a DAG file once and remember it. It re-parses every file on a loop, every few seconds to a minute depending on settings, to pick up changes. So any code at the top level, outside a task, runs on every single parse. If that code queries a database or calls an API, I'm hitting that system thousands of times a day even when nothing is scheduled, and parsing gets slow. Slow files delay scheduling for every DAG, and if a file takes too long it hits the import timeout, shows up as a broken DAG, and my changes stop being picked up. The same goes for Variable.get at the top level and heavy imports like big ML libraries. My rule is that top-level code only builds the graph. Anything that talks to the outside world goes inside a task, and variables come in through templates so they're read at run time."

Red flag to avoid:

Thinking a DAG file only executes when the DAG runs, so top-level code is harmless.

They may ask next:
  • How would you find which DAG files are slowest to parse?
  • Where would you put a heavy import like a machine learning library?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

4. How do you set dependencies between tasks, and when would you change a task's trigger rule?

What the interviewer is really testing:
Whether you can wire up a graph and handle the cases where a task should run even though something upstream failed or was skipped.
Answer frame:

Wiring: use the bitshift operators or chain; a list fans out or fans in.

Default rule: all_success, so a task runs only when every direct upstream task succeeded.

Other rules: all_done for cleanup, one_failed for alerts, none_failed_min_one_success after a branch.

Sample spoken answer:

"I set dependencies with the bitshift operators, so extract >> transform >> load, and a list on either side fans out or fans in. For long straight lines I use chain. Each task then has a trigger rule that decides when it may start, and the default is all_success: every direct upstream task must have succeeded. I change it in a few cases. A cleanup task that drops temp tables should run whatever happened, so it gets all_done. An alert task that should only fire when something breaks gets one_failed. The common trap is branching. After a branch operator, the path not taken is skipped, and with the default rule the join task would be skipped too. So the join gets none_failed_min_one_success, which means run if nothing failed and at least one branch actually ran."

Code:
extract >> transform >> [load_orders, load_customers] >> publish

cleanup = PythonOperator(
    task_id="cleanup_temp",
    python_callable=drop_temp_tables,
    trigger_rule="all_done",
)
publish >> cleanup
Red flag to avoid:

Not knowing the default is all_success, or being surprised that the task after a branch gets skipped.

They may ask next:
  • What happens to a downstream task when an upstream task is skipped under the default rule?
  • How does the always rule differ from all_done?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

5. What is the TaskFlow API, and how is it different from writing classic operators?

What the interviewer is really testing:
Whether you know the modern way to write Python tasks and understand what it does for you behind the scenes.
Answer frame:

Decorators: @dag and @task turn plain Python functions into a DAG and its tasks.

Data passing: a return value is pushed to XCom, and passing it into another task pulls it and sets the dependency.

Mixing: classic operators still work alongside, and their output can feed a TaskFlow task.

Sample spoken answer:

"TaskFlow lets me write tasks as normal Python functions with a @task decorator, and the DAG itself with @dag. The big difference is how data and dependencies flow. With classic operators, I'd create a PythonOperator, push a value to XCom, pull it in the next task by task id, and set the order separately with >>. With TaskFlow, a function's return value is pushed to XCom automatically, and when I pass that return value into another task function, Airflow pulls it for me and adds the dependency. So the code reads like ordinary Python. It's still XCom underneath, so the same rule applies: return small things like a count or a file path, not a big dataset. And I can mix styles freely, for example a SQL operator followed by a TaskFlow task that checks its result."

Code:
import pendulum
from airflow.decorators import dag, task

@dag(schedule="@daily", start_date=pendulum.datetime(2024, 1, 1, tz="UTC"), catchup=False)
def daily_orders():
    @task
    def extract() -> str:
        return "raw/orders.csv"

    @task
    def load(path: str) -> int:
        print(f"loading {path}")
        return 42

    load(extract())

daily_orders()
Red flag to avoid:

Thinking TaskFlow passes data in memory between tasks, when it's still XCom stored between separate processes.

They may ask next:
  • How would you pass the output of a classic operator into a TaskFlow task?
  • What happens if a TaskFlow task returns a very large DataFrame?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

6. How do you test DAGs before they reach production?

What the interviewer is really testing:
Whether you treat DAGs as code with a safety net, rather than finding mistakes when a scheduled run breaks.
Answer frame:

Import test: load every DAG file in CI and fail on any import error or cycle.

Logic tests: keep business logic in plain functions and unit test them without Airflow.

Run tests: run one task or a whole DAG locally, then deploy to a staging environment first.

Sample spoken answer:

"I test at three levels. First, a cheap check in CI that loads every DAG file, the same way the scheduler would, and fails if there's any import error, a cycle, or a DAG with no owner or no retries. That alone catches most broken deploys. Second, I keep the real logic in plain Python functions or modules that don't depend on Airflow, so I can unit test them with normal test data. Third, I actually run things. The tasks test command runs one task for a given date without recording state, and dag.test runs a whole DAG in a single process, which is great for debugging in an IDE. Before production, changes go to a staging Airflow pointed at test connections. I also add policy checks, for example that every DAG sets catchup explicitly, so risky defaults never slip through."

Code:
from airflow.models import DagBag

def test_no_import_errors():
    bag = DagBag(dag_folder="dags/", include_examples=False)
    assert bag.import_errors == {}

def test_every_dag_has_retries():
    bag = DagBag(dag_folder="dags/", include_examples=False)
    for dag_id, dag in bag.dags.items():
        assert dag.default_args.get("retries", 0) >= 1, dag_id
Red flag to avoid:

Saying the only test is deploying and watching the first run.

They may ask next:
  • How do you test a task that talks to a real database or cloud storage?
  • What would you check in CI to stop someone deploying a DAG with catchup left on by mistake?
Say it in 60 seconds

Operators & Hooks 4 questions

Easy Technical round Fresher, Mid-level Practice question

7. What's the difference between an operator, a sensor and a hook?

What the interviewer is really testing:
Whether you know the building blocks and which one to reach for when you write or read a DAG.
Answer frame:

Operator: a template for one unit of work; each instance in a DAG is a task.

Sensor: a special operator that waits until a condition is true, like a file arriving.

Hook: a reusable client for an external system, built on a connection, used inside operators and tasks.

Sample spoken answer:

"An operator is a template for one kind of work, like running a SQL statement, a bash command or a Python function. When I use one in a DAG with a task id, that instance becomes a task. A sensor is a kind of operator whose only job is to wait: it checks a condition, like a file landing in storage or a partition existing, and succeeds when it's true or fails after its timeout. A hook is lower level. It's the client that knows how to talk to an external system, like a database or a storage bucket, and it gets its host and credentials from an Airflow connection. Operators use hooks internally, and I use hooks directly inside my own Python tasks when no operator does quite what I need. So the hook talks to the system, the operator does the work, and the sensor waits."

Red flag to avoid:

Treating hooks and operators as the same thing, or not knowing that a hook gets its credentials from a connection.

They may ask next:
  • When would you write a hook of your own instead of using an existing one?
  • Why does a sensor that waits for hours cause problems if you're not careful?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

8. A sensor waits up to six hours for a partner's file. What's the difference between poke mode, reschedule mode and a deferrable sensor here?

What the interviewer is really testing:
Whether you understand how waiting tasks consume worker slots, which is a common cause of a jammed Airflow.
Answer frame:

Poke: the sensor holds a worker slot the whole time and sleeps between checks.

Reschedule: it checks, frees the slot, and gets rescheduled for the next check.

Deferrable: it hands the wait to the triggerer, which runs many async waits in one process, and resumes when the event fires.

Sample spoken answer:

"In poke mode the sensor starts on a worker and stays there, checking, sleeping for the poke interval, and checking again. For six hours that's a worker slot doing nothing. If twenty DAGs do that, my workers are full of sleeping sensors and real tasks queue behind them. Reschedule mode fixes the slot problem: the sensor checks once, and if the file isn't there it ends that attempt, goes into up_for_reschedule and frees the slot until the next check. That's good for long waits with a check every few minutes. A deferrable sensor goes further. It defers itself to the triggerer, which runs lightweight async triggers for many tasks in one process, and the task only comes back to a worker when the event fires. For a six-hour wait I'd use a deferrable sensor if one exists, otherwise reschedule, with a sensible timeout and an alert."

Red flag to avoid:

Leaving long sensors in poke mode without seeing that each one blocks a worker slot for the entire wait.

They may ask next:
  • What happens when the sensor's timeout is reached, and does it retry?
  • When is poke mode still the right choice?
Say it in 60 seconds
Medium Coding round Mid-level, Senior Practice question

9. Write a small custom operator that checks a table has rows for the run's date and fails the task if it doesn't.

What the interviewer is really testing:
Whether you can extend Airflow cleanly: subclass the base operator, use a hook for the connection, and make fields templatable.
Answer frame:

Subclass: inherit from BaseOperator and put the work in execute(self, context).

Hook: get the database client from a hook and a connection id, never hard-coded credentials.

Templates: list fields in template_fields so values like the run date can come from Jinja.

Sample spoken answer:

"I subclass BaseOperator and put the logic in execute, which Airflow calls with the run's context when the task runs. The constructor only stores arguments and passes the rest to the parent, so things like retries and task_id still work. For the database, I use the provider's hook with a connection id, so credentials live in the Airflow connection, not in my code. I add table and partition_date to template_fields, which means someone can pass the run date as a Jinja template and Airflow renders it just before execute. Inside, I run a count with a bound parameter. If the count is below the minimum, I raise an exception, which fails the task and triggers retries or alerts. I return the count, which goes to XCom in case a later task wants it. I keep execute small so the logic is easy to unit test."

Code:
from airflow.models import BaseOperator
from airflow.providers.postgres.hooks.postgres import PostgresHook

class RowCountCheckOperator(BaseOperator):
    template_fields = ("table", "partition_date")

    def __init__(self, table, partition_date, conn_id="warehouse", min_rows=1, **kwargs):
        super().__init__(**kwargs)
        self.table = table
        self.partition_date = partition_date
        self.conn_id = conn_id
        self.min_rows = min_rows

    def execute(self, context):
        hook = PostgresHook(postgres_conn_id=self.conn_id)
        # table comes from DAG code, never from user input
        sql = f"SELECT COUNT(*) FROM {self.table} WHERE load_date = %s"
        count = hook.get_first(sql, parameters=(self.partition_date,))[0]
        if count < self.min_rows:
            raise ValueError(f"{self.table} has {count} rows for {self.partition_date}")
        return count

# check = RowCountCheckOperator(task_id="check_orders", table="orders", partition_date="{{ ds }}")
Red flag to avoid:

Doing the database work in __init__, which runs on every DAG parse, instead of in execute.

They may ask next:
  • Why shouldn't the constructor open the database connection?
  • How would you make this operator skip the downstream tasks instead of failing?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

10. How do you handle connections, variables and secrets in Airflow so credentials never end up in DAG code?

What the interviewer is really testing:
Whether you can keep credentials safe and configuration out of code, and know the performance traps with variables.
Answer frame:

Connections: hooks read host and credentials by connection id; store them in a secrets backend, env vars or the metadata database.

Secrets backend: a vault or cloud secret manager keeps credentials out of Airflow's own database.

Variables: read them at run time through templates or inside tasks, never at the top level.

Sample spoken answer:

"Credentials never go in DAG files. Every hook takes a connection id, and the connection holds the host, login, password and extras. Where the connection lives is a deployment choice. It can be in the metadata database, which encrypts the password field with a key, or as an environment variable, but in production I prefer a secrets backend, like a vault or the cloud provider's secret manager, so secrets are rotated and audited in one place and Airflow just looks them up by name. Variables are for non-secret config like a bucket name. The trap is reading them at the top of a DAG file, which hits the database on every parse. I read them inside tasks or through templates like var.value.bucket_name, which only resolve at run time. Airflow also masks values that look sensitive in logs, but I don't rely on that alone."

Red flag to avoid:

Hard-coding passwords in the DAG or keeping them in plain variables and calling that secure.

They may ask next:
  • What happens to existing connections if the encryption key is lost or changed without rotation?
  • How would you give each environment, like dev and prod, different credentials without changing the DAG?
Say it in 60 seconds

XCom & Templating 2 questions

Easy Technical round Fresher, Mid-level Practice question

11. What is XCom, and what should you not pass through it?

What the interviewer is really testing:
Whether you know how tasks share small pieces of data and why XCom is not a data pipeline.
Answer frame:

Purpose: a key-value store for small messages between tasks, like a file path or a row count.

Storage: by default it lives in the metadata database, tied to the DAG run and task.

Limits: pass references, not datasets; use a custom backend or object storage for anything large.

Sample spoken answer:

"XCom, short for cross-communication, is how tasks pass small values to each other. Each task runs in its own process, maybe on a different machine, so they can't share memory. A task pushes a value, for example by returning it, and a later task pulls it by task id and key. By default those values are serialised and stored in the metadata database, attached to that run. That's why I only pass small things: a file path, a list of partition names, a row count, a status. I never pass a DataFrame or a full query result, because it bloats the database that the whole scheduler depends on, and there are size limits that depend on the database. For big data, the task writes it to storage and passes the path. If a team really needs larger XComs, you can set up a custom backend that stores them in object storage."

Red flag to avoid:

Saying you pass whole datasets between tasks with XCom.

They may ask next:
  • How does a task pull an XCom that a different task pushed with a custom key?
  • Why can pushing large XComs slow down the whole Airflow deployment, not just one DAG?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

12. How does templating work in Airflow? Where can you use something like the ds macro, and where won't it render?

What the interviewer is really testing:
Whether you can make tasks date-aware correctly, and know the common mistake of expecting Jinja to render everywhere.
Answer frame:

When: Jinja is rendered on the worker right before the task runs, using that run's context.

Where: only in an operator's template_fields and in template files like .sql it knows about.

Macros: ds, data_interval_start and data_interval_end, params and var give run-specific values.

Sample spoken answer:

"Airflow renders Jinja templates just before a task runs, using that run's context. So in a SQL operator I can write WHERE order_date = '{{ ds }}' and each run gets its own date, which is what makes reruns and backfills load the right slice. The key thing is that only fields listed in the operator's template_fields get rendered, plus template files with extensions it recognises, like a .sql file. If I put a Jinja string into a field that isn't templated, it arrives as literal braces. Inside a Python task, I don't use Jinja at all; I read the same values from the context, like data_interval_start. Useful values are ds for the date, data_interval_start and data_interval_end for the exact window, params for user settings, and var for variables. One catch: on Airflow 3 a plain cron schedule gives a zero-length interval by default, so for window queries like this one I make sure the DAG uses the interval timetable."

Code:
-- dags/sql/daily_orders.sql, rendered for each run
SELECT store_id, SUM(amount) AS revenue
FROM raw_orders
WHERE created_at >= '{{ data_interval_start }}'
  AND created_at <  '{{ data_interval_end }}'
GROUP BY store_id;
Red flag to avoid:

Using the system clock, like datetime.now(), inside a task to decide which data to load.

They may ask next:
  • How would you check what a template actually rendered to for a past run?
  • Why use data_interval_start and data_interval_end instead of today's date from the system clock?
Say it in 60 seconds

Scheduling & Backfill 4 questions

Medium Technical round Fresher, Mid-level Practice question

13. A DAG has a daily schedule and a start date of the first of January. When does its first run happen, what is its logical date, and does the Airflow version matter?

What the interviewer is really testing:
Whether you understand data intervals and timetables, the most common source of off-by-one-day bugs in Airflow.
Answer frame:

Airflow 2 default: a cron schedule uses data intervals, and a run starts only when its window closes.

First run there: just after midnight on January 2, with logical date January 1, the start of the window.

Airflow 3 default: cron schedules fire at their time, so the first run is at midnight on January 1, the logical date is the fire time and the interval has zero length.

Sample spoken answer:

"It depends on the timetable, and the default changed between versions. In Airflow 2, a cron schedule like daily uses data intervals: each run covers a window and only starts once that window has closed, because that's when a full day of data exists. So the first interval is January 1 midnight to January 2 midnight, the run starts just after midnight on January 2, and its logical date, the ds macro, is January 1. That's why people say Airflow runs a day late, but the run processes the day it's named after. Airflow 3 switched cron schedules to a trigger-style timetable by default. There the first run fires at midnight on January 1, its logical date is that fire time, and the data interval has zero length. So on Airflow 3, a daily job that loads yesterday either asks for the previous day itself or sets the interval timetable explicitly. I always check which one a project uses before reasoning about dates."

Red flag to avoid:

Saying the first run happens on January 1 and processes January 1 with no mention of data intervals, or not knowing the timetable decides it.

They may ask next:
  • Why is start_date equal to datetime.now() a bad idea?
  • On Airflow 3, how would you make a daily DAG load the previous full day?
  • If you change a DAG's start date to an earlier date, what happens?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

14. What does catchup do, and how is it different from running a backfill?

What the interviewer is really testing:
Whether you know how Airflow fills in missed runs and can control it on purpose instead of by accident.
Answer frame:

Catchup: when on, the scheduler creates a run for every missed interval since the start date or the last run.

Backfill: a deliberate request to run a DAG over a chosen date range, even with catchup off.

Habit: set catchup explicitly on every DAG, because the default is on in Airflow 2 and off in Airflow 3.

Sample spoken answer:

"Catchup is a DAG setting. When it's on and the scheduler sees intervals that have no run, say because the DAG was paused for a week or the start date is in the past, it creates a run for every one of those intervals. When it's off, it only schedules the most recent interval and moves on. A backfill is different: it's something I ask for. I pick a date range and Airflow creates runs for it, regardless of the catchup setting. In Airflow 2 that was a CLI command; Airflow 3 also lets you start one from the UI or the API, and the scheduler runs it. I use backfills after fixing a bug or adding a new DAG that needs history. Because catchup is on by default in Airflow 2 and off in Airflow 3, I always set it explicitly, and I keep max_active_runs low so a big catch-up doesn't flood the workers."

Red flag to avoid:

Not knowing that catchup can silently create hundreds of runs when you unpause or deploy a DAG.

They may ask next:
  • What happens if you unpause a DAG with catchup on after it's been paused for three months?
  • How is clearing past task instances different from a backfill?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

15. One team's DAG loads raw orders, and your DAG should only start after that load finishes. How would you set up that dependency across DAGs?

What the interviewer is really testing:
Whether you know the options for linking DAGs and can weigh coupling, timing and reliability between them.
Answer frame:

Data-aware schedule: the producer declares a dataset or asset it updates, and my DAG is scheduled on it.

Sensor: an external task sensor waits for a specific task in the other DAG for a matching logical date.

Push: the producer triggers my DAG directly with a trigger operator.

Sample spoken answer:

"There are three common ways. My first choice is data-aware scheduling. The producer's load task declares that it updates a dataset, called an asset in newer versions, identified by a URI like the orders table, and my DAG uses that dataset as its schedule. When the producer task succeeds, my DAG runs. It's loosely coupled: they don't need to know each other's DAG ids or schedules. The second option is an external task sensor in my DAG that waits for a specific task in theirs. It works, but it's tied to their task id and assumes the logical dates line up, so if schedules differ I have to map the dates, and a renamed task breaks my DAG silently. The third is having their DAG trigger mine directly, which puts the dependency in their code. One caution with datasets: they fire when the task succeeds, not when data actually changed, so I still validate the data."

Red flag to avoid:

Solving it with a fixed time gap, like scheduling one DAG two hours after the other and hoping.

They may ask next:
  • With an external task sensor, what goes wrong if the two DAGs run on different schedules?
  • How would you stop your DAG running twice if the producer is re-run the same day?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level, Senior Practice question

16. This task appends yesterday's orders with a plain INSERT. What goes wrong on a retry or a rerun, and how would you rewrite it?

What the interviewer is really testing:
Whether you can turn a non-repeatable task into one that gives the same result however many times it runs.
Answer frame:

Problem: a retry after a partial write, or a later clear, inserts the same rows again.

Scope: tie the task to its run's window with templates, never to the current clock.

Rewrite: delete and insert that window in one transaction, overwrite the partition, or MERGE on a key.

Sample spoken answer:

"A plain INSERT isn't idempotent. If the task fails after writing, the retry inserts the same rows again. If someone clears the task next week, or runs a backfill, it adds another copy. And if the query picks yesterday using the current date, a rerun next week loads the wrong day entirely. I'd fix two things. First, scope the work to the run's own window using ds or the data interval, so any run always touches exactly its own day. Second, make the write replace that day instead of adding to it: delete the day's rows and insert them again inside one transaction, or overwrite the partition in a lake table, or use a MERGE keyed on the order id. Then I can retry, clear or backfill any date and the table ends up exactly the same."

Code:
-- ds is the day this run covers (interval timetable)
BEGIN;

DELETE FROM sales_daily
WHERE order_date = '{{ ds }}';

INSERT INTO sales_daily (order_date, store_id, revenue)
SELECT order_date, store_id, SUM(amount)
FROM raw_orders
WHERE order_date = '{{ ds }}'
GROUP BY order_date, store_id;

COMMIT;
Red flag to avoid:

Using CURRENT_DATE or datetime.now() to pick the day, which breaks every rerun and backfill.

They may ask next:
  • What if the task also sends an email or calls a payment API? How do you make that part safe to retry?
  • Why is the delete and insert wrapped in one transaction?
Say it in 60 seconds

Retries & Alerts 3 questions

Easy Technical round Fresher, Mid-level Practice question

17. How do you set up retries for a task that calls a flaky external API?

What the interviewer is really testing:
Whether you know the retry settings and can tell apart failures worth retrying from ones that should fail fast.
Answer frame:

Settings: retries, retry_delay, exponential backoff and a max delay, usually in default_args.

Time limit: execution_timeout so a hung call fails instead of blocking a slot for hours.

Judgement: retry transient errors; fail fast on bad input or bad credentials.

Sample spoken answer:

"I set retry behaviour in default_args so every task in the DAG gets it, and override it per task when needed. For a flaky API I'd give it a few retries with a retry delay of a few minutes, turn on exponential backoff so each wait is longer than the last, and cap it with max_retry_delay so it doesn't wait forever. I also set execution_timeout, because a request that hangs is worse than one that fails: it holds a worker slot and never triggers the retry. Retries only help with transient problems like timeouts or rate limits. If the API says my credentials are wrong or the payload is invalid, retrying just delays the alert, so for those I raise an exception that fails the task immediately without retrying. And because the task may run more than once, it has to be safe to repeat."

Code:
from datetime import timedelta

default_args = {
    "owner": "data-platform",
    "retries": 3,
    "retry_delay": timedelta(minutes=5),
    "retry_exponential_backoff": True,
    "max_retry_delay": timedelta(minutes=30),
    "execution_timeout": timedelta(hours=1),
}
Red flag to avoid:

Setting a large number of retries on everything so failures never show, without any timeout.

They may ask next:
  • How would you make a task fail straight away without using up its retries?
  • Where would you send an alert: on every retry, or only on the final failure?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

18. What's the difference between execution_timeout, a sensor's timeout and dagrun_timeout?

What the interviewer is really testing:
Whether you know which limit applies to what, so you can stop hung work without killing healthy runs.
Answer frame:

execution_timeout: caps one attempt of one task; hitting it fails that attempt, and retries still apply.

Sensor timeout: caps the total time a sensor waits for its condition; hitting it fails the sensor without retrying.

dagrun_timeout: caps how long a whole DAG run may stay running before it's marked failed.

Sample spoken answer:

"They work at different levels. execution_timeout is per task attempt. If one attempt runs longer, Airflow kills it and marks that attempt failed, and normal retries kick in, so it's my protection against a hung query or API call. A sensor's timeout is about the condition, not one attempt: it's the total time the sensor is allowed to wait for, say, a file to arrive. When that's exceeded, the sensor fails and doesn't use its retries, because waiting again wouldn't make the file appear. That's different from execution_timeout on a sensor, which would limit a single attempt. dagrun_timeout sits on the DAG and limits how long a whole run can stay running before it's marked failed, which also frees a slot when max_active_runs is low. I usually set all three: tight per task, realistic for sensors, and a DAG-level ceiling as a safety net."

Red flag to avoid:

Treating the three as interchangeable, or expecting a timed-out sensor to retry automatically.

They may ask next:
  • For a sensor in reschedule mode, is the timeout counted per check or across all checks?
  • What happens to tasks still running when dagrun_timeout is hit?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

19. Failures send you an alert, but how would you find out that a DAG is running late or never started at all?

What the interviewer is really testing:
Whether you think beyond failure callbacks to the silent problems: slow runs, a stuck scheduler, a run that never began.
Answer frame:

Failure hooks: on_failure_callback or a notifier for failures and final retries.

Lateness in Airflow: Airflow 2 has a task-level sla setting; Airflow 3 removed it, and deadline alerts take its place in later 3.x releases.

Outside check: a separate freshness check on the output, so a dead scheduler still raises an alarm.

Sample spoken answer:

"Failure alerts are the easy part: on_failure_callback, or a notifier, posts to chat or pages whoever is on call. Lateness is harder, because nothing fails. Inside Airflow 2 there's an sla argument on tasks with an SLA miss callback, but it had quirks and Airflow 3 removed it, with deadline alerts replacing it in later releases, so I check the version before relying on either. The bigger gap is that any alert that lives in Airflow depends on Airflow working. If the scheduler is down, no run starts, and no callback ever fires. So for important pipelines I add a check from outside: a small monitor that looks at the output, for example the newest date in the target table, and alerts if it's older than expected by a set time. I also monitor the scheduler's heartbeat. That way I hear about late data whatever the cause."

Red flag to avoid:

Relying only on failure emails and never considering that a run might simply not start.

They may ask next:
  • What would you put in a failure alert so the on-call person can act without opening five tools?
  • How do you avoid alert fatigue when a DAG retries three times before failing?
Say it in 60 seconds

Executors & Scaling 3 questions

Medium Technical round Mid-level, Senior Practice question

20. Compare the Local, Celery and Kubernetes executors. How would you choose between them?

What the interviewer is really testing:
Whether you understand how tasks are actually run and the trade-offs in cost, isolation and operations.
Answer frame:

Local: tasks run as processes on the scheduler's machine; simple, limited to one box.

Celery: tasks go through a queue to a pool of long-running workers; fast start, needs a broker and shared dependencies.

Kubernetes: each task gets its own pod; strong isolation and per-task resources, slower start.

Sample spoken answer:

"The executor decides how a task instance gets run. The Local executor runs tasks as separate processes on the same machine as the scheduler. It's simple and fine for a small team, but it can't scale past one box. The Celery executor puts tasks on a message queue, usually Redis or RabbitMQ, and a pool of always-on workers picks them up. Tasks start fast, I can add workers, and I can route tasks to named queues, like a queue of big-memory workers. The downside is running the broker and keeping every worker's dependencies in sync. The Kubernetes executor launches a fresh pod for each task. That gives isolation, per-task images and resources, and nothing running when idle, but each task pays pod start-up time. So for many short tasks I lean Celery, and for mixed, heavy or dependency-clashing workloads, Kubernetes. Newer versions also let you run more than one executor together."

Red flag to avoid:

Saying Kubernetes is always better, without mentioning pod start-up cost or the work of running a cluster.

They may ask next:
  • How would you give one memory-hungry task more resources under each executor?
  • What happens to tasks that were running on a Celery worker when that worker dies?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

21. Five DAGs all read from the same production database, and the DBA says Airflow is overloading it. How do you limit that?

What the interviewer is really testing:
Whether you know Airflow's concurrency controls and pick the one that matches the real bottleneck, a shared resource.
Answer frame:

Pool: create a pool with a fixed number of slots and assign every task that hits that database to it.

DAG limits: max_active_runs and max_active_tasks cap how much one DAG does at once.

Priority: priority_weight decides which waiting task gets the next free slot.

Sample spoken answer:

"The tool for a shared resource is a pool. I'd create a pool for that database with, say, four slots, agreed with the DBA, and put every task that queries it into that pool, across all five DAGs. Then no matter how many runs are active, at most four of those queries run at once, and the rest wait in the scheduled state until a slot frees up. A task that's especially heavy can take more than one slot with pool_slots. I'd also look at the DAG-level limits: max_active_runs so a backfill doesn't start thirty days in parallel, and max_active_tasks to cap tasks per DAG. If some queries matter more, like the one feeding a morning report, I give them a higher priority_weight so they get slots first. Finally I'd check the queries themselves, because sometimes the real fix is one bad full-table scan."

Red flag to avoid:

Answering only with bigger database hardware or fewer DAGs, without knowing pools exist.

They may ask next:
  • How is a pool different from the global parallelism setting?
  • How would you size the pool, and how would you know it's right?
Say it in 60 seconds
Hard Technical round Senior Practice question

22. Tasks are sitting in the queued or scheduled state for a long time and nothing is failing. How do you work out why?

What the interviewer is really testing:
Whether you can debug capacity problems layer by layer instead of restarting things at random.
Answer frame:

Which state: stuck in scheduled points at Airflow's own limits; stuck in queued points at the executor or workers.

Airflow limits: global parallelism, pool slots, max_active_tasks and max_active_runs.

Executor side: worker count and concurrency, queue names, broker health, or pods that can't be placed.

Sample spoken answer:

"First I check which state they're stuck in, because it tells me where to look. Scheduled means the scheduler knows the task is ready but hasn't handed it to the executor, usually because of a limit: the global parallelism setting, a full pool, or the DAG's max_active_tasks. So I check pools in the UI and count running tasks. Queued means it has been sent to the executor but no worker has started it. With Celery, I check that workers are alive, that their concurrency isn't used up by long sensors in poke mode, and that some worker actually listens on the queue the task was sent to, which is a classic mistake. I also check the broker. With Kubernetes, I look for pods stuck pending because the cluster has no room or can't pull the image. Finally I check scheduler health: its heartbeat, CPU and how long DAG parsing takes."

Red flag to avoid:

Restarting the scheduler first and hoping, without checking pools, limits or worker health.

They may ask next:
  • How can slow DAG file parsing make every DAG look slow to start?
  • What would you change long term so this doesn't happen during the busy midnight window?
Say it in 60 seconds

Dynamic DAGs 2 questions

Medium Coding round Mid-level, Senior Practice question

23. Each day a variable number of files lands in a folder. Write a DAG that processes every file in parallel, then summarises the results.

What the interviewer is really testing:
Whether you know dynamic task mapping, the built-in way to create a runtime-decided number of parallel tasks.
Answer frame:

List: one task finds the files at run time and returns their paths.

Expand: the processing task is mapped with expand, so Airflow creates one instance per path.

Reduce: a downstream task receives all the mapped results; a concurrency limit protects the systems involved.

Sample spoken answer:

"This is exactly what dynamic task mapping is for. The first task runs at run time, lists the files for that day, and returns a list of paths. Then I call expand on the processing task with that list, and Airflow creates one mapped task instance per path, all in the same run, each with its own logs and retries. So if one file fails, only that instance retries. A final task takes the mapped results and gets them as a list, so it can add up counts or send a summary. I'd put a limit on how many mapped instances run at once so a day with five hundred files doesn't swamp the workers. The nice part compared to older tricks is that the number of tasks isn't fixed when the DAG is parsed; it's decided by the data each run."

Code:
import pendulum
from airflow.decorators import dag, task

@dag(schedule="@daily", start_date=pendulum.datetime(2024, 1, 1, tz="UTC"), catchup=False)
def process_new_files():
    @task
    def list_files() -> list[str]:
        # real code: list the day's keys with a storage hook
        return ["in/a.csv", "in/b.csv", "in/c.csv"]

    @task(max_active_tis_per_dag=4)
    def process(path: str) -> int:
        print(f"processing {path}")
        return 1

    @task
    def summarize(counts):
        print(f"processed {sum(counts)} files")

    summarize(process.expand(path=list_files()))

process_new_files()
Red flag to avoid:

Looping over files at the top level of the DAG file, which lists storage on every parse.

They may ask next:
  • What happens if list_files returns an empty list?
  • How would you pass a fixed argument to every mapped instance alongside the changing path?
Say it in 60 seconds
Hard Technical round Senior Practice question

24. Your team wants one DAG per source system, generated from a config file with fifty entries. How would you build that, and what can go wrong?

What the interviewer is really testing:
Whether you can generate DAGs safely and know the parsing, history and blast-radius problems it brings.
Answer frame:

Build: one Python file loops over a config file shipped with the code and creates a DAG per entry with a stable dag_id.

Risks: slow parsing, one bad entry breaking all fifty, orphaned history when ids change.

Alternative: if only the data varies inside one run, dynamic task mapping may be the better fit.

Sample spoken answer:

"I'd write one generator file reading a YAML or JSON config deployed with the code, loops over the entries, and builds a DAG for each with a factory function, with a dag_id like ingest_ plus the source name. Each DAG has to be registered where the parser finds it, so I build it in a with block or put it in globals(). Then I'd guard against the known risks. The config must be a local file, not a database or API call, because it's read on every parse. I'd validate the config in CI so one typo doesn't turn the whole file into an import error and take all fifty DAGs down together. dag_ids must never change, because a renamed DAG loses its run history and starts again from its start date, possibly with catchup. And removing an entry leaves an old DAG in the UI that someone has to clean up. If the fifty sources really share one schedule, I'd ask whether one DAG with mapped tasks is simpler."

Red flag to avoid:

Reading the config from a live database at parse time, or generating dag_ids that change when the config is reordered.

They may ask next:
  • How would you let one source have a different schedule or retry setting?
  • How would you split parsing if the generator file becomes slow?
Say it in 60 seconds

Production Work 6 questions

Medium Behavioral round Mid-level, Senior Practice question

25. Tell me about an Airflow DAG that kept failing now and then. How did you find the real cause?

What the interviewer is really testing:
Whether you debug intermittent failures with evidence, and fix the cause rather than just adding retries.
Answer frame:

Situation: which DAG, how often it failed, what it cost the people using the data.

Evidence: logs across many failed runs, timing, what the failures had in common.

Fix and follow-through: the real fix, what you changed to catch it sooner, and the result.

Sample spoken answer:

"At my last company we had a nightly DAG that failed maybe twice a week at the load step, and the usual fix was someone clearing the task in the morning. Retries had been bumped up, which only hid it. I pulled the logs from a month of failed runs and noticed they all failed around the same few minutes, and always with a connection error. That lined up with the warehouse's maintenance window, and on busy nights our run slipped into it because an upstream sensor released late. So the cause wasn't the code, it was timing. I moved the heavy step off that window, added a pool so our loads couldn't open too many connections at once, and made the load a partition overwrite so retries were safe. Failures stopped, and I added a dashboard of task durations so drift like that shows up before it breaks."

Red flag to avoid:

A story where the fix was just more retries, with no idea why it failed.

They may ask next:
  • Looking back, what would you have checked first?
  • How did you convince the team that more retries wasn't the fix?
Say it in 60 seconds
Hard Behavioral round Senior Practice question

26. Tell me about a time you upgraded Airflow or moved DAGs to a new deployment. How did you avoid breaking production?

What the interviewer is really testing:
Whether you can plan risky platform work: inventory, testing, rollout and rollback, with the teams who own the DAGs.
Answer frame:

Scope: what was changing and why, how many DAGs and teams were affected.

Safety: inventory of deprecated features, a parallel environment, CI checks, a rollback plan.

Rollout: the order you moved things, how you handled schedules and catchup, and what you learned.

Sample spoken answer:

"In my last role I led moving about a hundred and twenty DAGs from an old self-managed Airflow to a newer version on a managed platform. First I built an inventory by running every DAG file through the new version's parser in CI and listing the import errors and deprecation warnings: old import paths, the execution_date name, a couple of custom plugins. Owners fixed their DAGs against that list. We ran both environments side by side, with the new one reading from staging connections, and compared outputs for a week. For the cutover, the dangerous part was scheduling: a DAG that appears in a new environment with catchup on will try to run its whole history. So every DAG went across paused with catchup set explicitly, and we unpaused team by team while the old copy was paused. We kept the old deployment for two weeks as a rollback. The only incident was one missed connection, which the run check caught."

Red flag to avoid:

Describing a big-bang switch over a weekend with no parallel run, no inventory and no rollback.

They may ask next:
  • What would you do differently on the next upgrade?
  • How did you get dozens of DAG owners to fix their code on time?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

27. Tell me about a time you set standards or reviewed DAGs for a team. What did you change, and did people follow it?

What the interviewer is really testing:
Whether you can raise the quality of a shared Airflow codebase through habits and tooling, not only by fixing things yourself.
Answer frame:

Problem: what kept going wrong across the team's DAGs.

Standard: a short set of rules, a template, and checks in CI so the rules enforce themselves.

Adoption: how you brought people along and what changed afterwards.

Sample spoken answer:

"At my last company, about a dozen engineers wrote DAGs and the same mistakes kept coming back: no retries, catchup left to the default, API calls at the top of files, and INSERTs that duplicated data on retry. Instead of catching these one by one in review, I wrote a one-page guide and a starter DAG template with sensible default_args, catchup set explicitly, and an idempotent write pattern. Then I added CI checks that loaded every DAG and failed on import errors, missing owners, missing retries and slow parse times. I walked the team through it in a short session and fixed the three worst existing DAGs myself as examples. People followed it mostly because the template was the easiest place to start. Over the next quarter, duplicate-row incidents stopped and review comments were about logic, not the same basics."

Red flag to avoid:

A story about rules that were only written down and never checked, or about fixing everyone's DAGs yourself.

They may ask next:
  • Which rule got the most pushback, and how did you handle it?
  • How do you keep a standard like that from going out of date?
Say it in 60 seconds
Hard Situational round Mid-level, Senior Practice question

28. The midnight run of a DAG that feeds morning finance dashboards has failed, and you're on call. It's 1 a.m. and people read the dashboards at 8. Walk me through what you do.

What the interviewer is really testing:
Whether you stay calm and methodical under time pressure, protect data correctness, and keep the business informed.
Answer frame:

Triage: which task failed, its logs, and whether the cause is transient, code, data or upstream.

Safe rerun: check what was partly written, confirm the tasks are idempotent, then clear the failed task and its downstream.

Communicate: if it won't be fixed by 8, tell the dashboard owners early and mark the data as stale.

Sample spoken answer:

"First I look at which task failed and read its log from the last attempt, not the first. Then I sort the cause: a timeout or connection error is probably transient; a schema or key error means the data changed upstream; a code error means a recent deploy. I also check if the source even delivered. Before I rerun anything, I check what the failed task wrote. If the tasks overwrite their partition, I can safely clear the failed task with its downstream tasks and let them run. If not, I clean up the partial data first. If the source is late or broken, I contact whoever owns it and wait rather than load half a day. By about 6, if I'm not confident it'll be fixed, I message the finance owners so they know the numbers are stale. I never mark the task as success to turn the dashboard green. Afterwards, I write up what happened and fix the root cause."

Red flag to avoid:

Marking the failed task as success to unblock downstream tasks, or rerunning everything without checking what was partly written.

They may ask next:
  • The source file is simply missing. Do you run the DAG with yesterday's data or wait?
  • What would you change so the next failure pages someone before midnight turns into morning?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

29. Someone deployed a DAG with catchup on and a start date two years in the past. Hundreds of runs are now queuing and hitting the warehouse. What do you do?

What the interviewer is really testing:
Whether you contain the damage first, then clean up carefully, and think about side effects of the runs that already happened.
Answer frame:

Stop: pause the DAG right away so no new runs or tasks start.

Assess: see which runs already ran, what they wrote or sent, and whether that data is wrong.

Clean up and prevent: remove or fail the unwanted runs, fix catchup and the start date, add a CI check.

Sample spoken answer:

"First I pause the DAG, which stops new task instances starting, and if the warehouse is struggling I'd also check with its admins whether to cancel running queries. Then I assess. Which runs actually completed, and what did they do? If the tasks only overwrite their own day's partition, old runs may have done little harm. If they appended, sent emails or called external systems, I need a list of affected dates to clean up or to warn people about. Next, I deal with the queued runs, by marking them failed or deleting them in bulk from the UI, so they don't all start when I unpause. Then I fix the DAG: catchup off, a realistic start date, and a max_active_runs limit. If some history really is needed, I run a planned backfill later, in small batches. Finally I'd add a CI rule that every DAG sets catchup explicitly."

Red flag to avoid:

Unpausing again after deleting a few runs, or leaving it paused without checking what the finished runs already wrote.

They may ask next:
  • If the business actually needs six months of that history, how would you backfill it safely?
  • How would you explain what happened to the team whose warehouse was overloaded?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

30. A teammate wants to load a large file into pandas inside a Python task and do all the transformation on the Airflow workers. What's your view?

What the interviewer is really testing:
Whether you see Airflow as an orchestrator and can suggest the right place for heavy compute without being dogmatic.
Answer frame:

Principle: Airflow schedules and coordinates; heavy compute belongs in a warehouse, a Spark job or its own container.

Risk: big pandas jobs eat worker memory, get killed, and slow every other DAG sharing those workers.

Pragmatism: small data in a Python task is fine; agree a size threshold and a path for bigger jobs.

Sample spoken answer:

"I'd start by asking how big the data is and how much it'll grow, because for a few hundred megabytes a Python task is perfectly fine and simpler. But for large files I'd push back. Airflow is best as the orchestrator: it decides when work runs, retries it and tracks it, while the heavy lifting happens somewhere built for it. Workers are shared, so a pandas job that needs lots of memory can get killed mid-run, and it slows down every other team's tasks on the same worker. So I'd suggest either doing the transform in the warehouse with SQL, which Airflow triggers with an operator, or running it in a separate container or Spark job with its own resources, which Airflow launches and waits for. I'd frame it as a team guideline with a rough size threshold, not a blanket ban, so the choice is easy next time."

Red flag to avoid:

Either saying it's always fine because it works on a laptop, or banning Python tasks outright with no reason.

They may ask next:
  • If you use Kubernetes, how would you give that one task its own resources?
  • How would you decide the size where a Python task stops being a good idea?
Say it in 60 seconds
Were you asked something else? Share it A person checks every question before it goes on the site. No name is shown.
For the call itself

The questions above are the prep. The call has ten more.

ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.

Download ClapAssist with 10 free minutes
Mac and Windows · Stays out of screen share · No card