Airflow triggers work; it is not a compute engine. Submitting to Spark, EMR and Databricks, KubernetesPodOperator, and dependency isolation.
⏱ 11 min readTopics chapter readerLevel · Medium
01 · Start here
Start Here
⏱ 5 min · Topic 1 of 9
Airflow orchestrates: it decides what runs, and when. It should not do the heavy computing itself.
This chapter shows what that rule means for real tasks. Heavy work runs on another system. The Airflow task starts it, waits for it, and records how it ended.
You keep the data from the last chapters: the DAG shop_orders_daily. You also meet a made-up heavy job, orders_history_rollup. It reads one year of orders.
Every number about that job is made up. You answer four questions:
Why can the Airflow worker not run a heavy job?
Which operators start work somewhere else?
How does a task wait without wasting a worker slot?
What happens to the job when the task fails?
02 · The problem
The Worker Is Too Small
⏱ 8 min · Topic 2 of 9
A worker is the machine that runs your tasks. PythonOperator and BashOperator run on the worker, inside its memory.
Take a made-up worker with 8 GiB of memory. A GiB is about one billion bytes.
A slot is a place for one running task. The worker has 16 slots, the default worker_concurrency from Executors Compared: Local, Celery, Kubernetes.
Now the made-up job orders_history_rollup. It reads 365 daily order files of 0.2 GiB each. Look at the numbers:
One worker against one heavy job (all numbers made up)
Item
Value
How we got it
Memory of the worker
8 GiB
Given
Memory per slot, on average
0.5 GiB
8 divided by 16 slots
Size of one year of orders
73 GiB
365 files times 0.2 GiB
Job size against the worker
More than 9 times
73 divided by 8 is about 9.1
This assumes the task loads the whole year into memory, for example with pandas.
The task cannot fit. The system kills it when the memory is full, and the task fails.
You could buy bigger workers. But then every worker must be big, all day, for one job that runs a few hours.
Spark solves this by splitting the data. A Spark cluster has a driver that plans the job and executors that do the work. A cluster manager such as YARN or Kubernetes gives it machines.
Say a made-up cluster has 20 executors with 8 GiB each. Each one holds 73 divided by 20, so 3.65 GiB of the year. That fits.
The Airflow task only starts the job and waits. Deploying Spark: spark-submit, YARN & Kubernetes teaches how the cluster runs and how to start it.
Remember
Small work can stay on the worker. The rule is about size. Ask of every task: is it coordinating, or computing?
03 · Choose a way
Where Each Operator Runs
⏱ 7 min · Topic 3 of 9
Airflow has an operator for each place where work can run. Some run on the worker. Others start the work somewhere else.
Look at where the work happens for each one. A container is a sealed package of a program and its libraries. A pod is a small box of containers that Kubernetes runs:
Operator
Where the work runs
What it needs
BashOperator
On the worker
The command and tools on the worker
PythonOperator
On the worker
Your libraries on the worker
DockerOperator
In a container on a Docker host
Access to Docker from the worker
KubernetesPodOperator
In a new pod in a Kubernetes cluster
Access to the cluster
SparkSubmitOperator
On a Spark cluster, started by spark-submit on the worker
Spark files on the worker and a connection
Cloud operators
In a cloud service
A connection with credentials
A connection stores the address and the login of another system. Connections, Variables & Secrets Management taught it.
KubernetesPodOperator starts a pod for the work of one task, under any executor.
KubernetesExecutor is different. It starts a pod for every task. Executors Compared: Local, Celery, Kubernetes compares them.
SparkSubmitOperator runs the spark-submit program on the worker. The program is small, but it must be installed there. The task stays on the worker until the Spark job ends, as the next section shows.
Cloud operators call the API of a service, its interface for programs. Three examples, from the providers. Check your provider version for the exact options:
EmrAddStepsOperator adds a step to an Amazon EMR cluster. EmrStepSensor waits for it.
EmrServerlessStartJobOperator starts a job on EMR Serverless.
DatabricksSubmitRunOperator sends a one-time run to Databricks. DatabricksRunNowOperator starts an existing job by its job_id.
Watch out
DockerOperator needs the worker to reach a Docker host. By default it uses the Docker socket, the door to the Docker program. Giving a worker that socket is a security risk, because it gives control over the host.
04 · Wait cheaply
Submit, Then Wait Cheaply
⏱ 9 min · Topic 4 of 9
After the job starts, the task must wait for the result. Downstream tasks need the output. So the task should end only when the job ends.
If the task waits by polling, it holds its slot for the whole job. Polling means asking again and again: are you done yet?
A deferrable task hands its wait to the triggerer, the Airflow part that waits for events. Concurrency, Pools, Priorities & Deferrable Operators teaches it.
Now count. The made-up job orders_history_rollup takes 2 hours, and ten such jobs run each night. Compare two ways to wait:
Ten jobs of 2 hours each (made up)
How the task waits
Slot time per job
Slots held at once
Total slot time
Blocking: the task polls
2 hours
10 of 32
20 hours
Deferrable: the triggerer polls
About 10 seconds
Almost none
About 100 seconds
The 32 is the default parallelism, the most tasks that run at once. For deferrable, 5 seconds to submit and 5 seconds to finish are made up.
In the blocking row, 10 slots times 2 hours is 20 hours. Those 10 slots do nothing but wait, and 22 slots are left for real work.
In the deferrable row, 10 jobs times 10 seconds is 100 seconds.
Many provider operators accept deferrable=True. Not every operator does, so check the provider documentation for your version. Here is one for Databricks:
Start a Databricks run and wait deferrablyworked example
The job_id 7 is made up. The date comes from the template {{ ds }}, so a backfill reads the right day.
Do not free the slot by not waiting. Some operators end as soon as the job is accepted. EmrAddStepsOperator does this by default: its wait_for_completion is False.
Then downstream tasks start with no output. If the job fails later, the Airflow run still shows green. So add EmrStepSensor, or set wait_for_completion=True or deferrable=True.
Watch out
SparkSubmitOperator has no deferrable option. It holds its worker slot until the Spark job ends. In cluster mode the task still waits, because spark-submit or the operator keeps checking the job.
05 · Your own code
Pods for Your Own Code
⏱ 6 min · Topic 5 of 9
Sometimes the heavy work is your own Python code, not a Spark job. A pod gives that code its own image, its own memory and its own libraries.
An image is a saved package with your code and libraries. Now a made-up task, orders_month_rollup. It reads one month: 31 files of 0.2 GiB, so 6.2 GiB.
That does not fit a worker slot of 0.5 GiB. It fits a pod with 16 GiB. Look at the operator:
The limit depends on your metadata database, so keep XComs small. When something fails, you need the logs. They live in different places:
Where the work ran
Where to read the log
On the worker
The Airflow task log
In a pod
The task log, if get_logs=True. Otherwise the pod log, until the pod is deleted.
On a Spark cluster
The Spark UI or the history server, and the cluster manager logs
In a cloud service
The run page of that service
The Airflow log often shows only that the job failed. The real error is in the other system. So log the job URL or id right after you submit.
07 · Failures
When the Task Dies, the Job Lives
⏱ 7 min · Topic 7 of 9
Airflow keeps a job id. It does not keep the job. If the task is killed, the remote job often keeps running.
Follow the task orders_history_rollup in a made-up run, dag_run 430. A heartbeat is a regular "I am alive" signal from a running task. The worker dies 30 minutes in, and Airflow retries the task:
A retry that starts a second job (made up)
Time
What happens
Airflow try
Remote job
00:00:10
The task submits the job
Try 1
job-431 starts
00:30:00
The worker dies. Its heartbeats stop.
Try 1 running
job-431 keeps running
00:35:00
The scheduler marks the task failed
Try 1 failed
job-431 keeps running
00:40:00
The retry submits the job again
Try 2
job-432 starts
02:00:10
The first job ends
Try 2 waits
job-431 writes its result
02:40:00
The second job ends
Try 2 succeeds
job-432 writes its result
Each job takes 2 hours. The heartbeat timeout and the retry_delay are 5 minutes each, made up. Airflow shows one green run, but two jobs wrote.
What the two writes do depends on how the job writes. Take 2026-01-15 from the shop data: its revenue is 15.50. Look at the code, then at the table after both jobs:
Append or overwrite one dayworked example
Python
# Append: adds the rows again
daily.write.mode("append").insertInto("orders_rollup")
# Overwrite one day: replaces that day only
spark.conf.set("spark.sql.sources.partitionOverwriteMode", "dynamic")
daily.write.mode("overwrite").insertInto("orders_rollup")
Result · 2 rows
write mode
rows for 2026-01-15
revenue summed
append
2
31.00
overwrite one day
1
15.50
The sum in the append row is 15.50 plus 15.50. It is double the true revenue.
The table orders_rollup is split by order_date and holds one row per day. Both jobs write the same row for 2026-01-15.
This is idempotency from Idempotency & Safe Task Design: running twice gives the same result as running once. It protects the data even when cancellation fails. Two more defences help:
on_kill: Airflow calls this method of the operator when it stops a task. A good operator cancels the remote job there. Not all operators do.
Reattach: some operators save the job id and reconnect to it on retry. KubernetesPodOperator does this by default with reattach_on_restart. Newer Airflow releases may extend this, so check your version.
A hard kill of the worker may never call on_kill. So do not rely on cancellation alone.
Interview question
Your Airflow task retried and the Spark job ran twice. How do you protect the data? A strong answer starts with an idempotent write, then adds cancellation and reattach.
08 · Recap
Quick Recap
⏱ 4 min · Topic 8 of 9
Airflow submits the heavy work, waits cheaply, and records the result. Look at the answers you should know:
Question
Short answer
Where does heavy computing belong?
Outside Airflow: Spark, a cloud service or a pod.
Where do BashOperator and PythonOperator run?
On the worker.
Does KubernetesPodOperator need KubernetesExecutor?
No. It works under any executor.
How does a task wait without holding a slot?
Deferrable mode, where the operator offers it.
Why not end the task when the job is accepted?
Downstream tasks would start with no output.
What do you pass between tasks?
A reference, such as a folder path.
What if the task dies mid-job?
The remote job may keep running. Make it idempotent.
Try this alone. A task loads 40 GiB with pandas on a worker with 8 GiB. Where should it run, and how should the task wait?
The answer is a Spark cluster or a large pod. The task should wait in deferrable mode, if the operator offers it.
09 · Next Chapter
Next Chapter
You now know how Airflow hands heavy work to other systems. The next chapter covers how DAGs reach production without breaking it.
⏱ 3 min · Topic 9 of 9
Next chapter
Testing DAGs & CI/CD Deployment
You now know how Airflow hands heavy work to other systems. The next chapter covers how DAGs reach production without breaking it.
It teaches the layers of DAG testing, the integrity test, and the ways DAG code reaches a deployment.