How each executor actually runs a task, what it costs, and a decision framework that starts from your workload shape rather than fashion.
⏱ 11 min readTopics chapter readerLevel · Medium
01 · Start here
Start Here
⏱ 5 min · Topic 1 of 10
The last chapter said an executor sends a task out to run. One line in the config picks which kind of executor you use.
That one line also decides what you must deploy, and what you pay for. This chapter compares the main kinds, then shows how to choose.
You follow the same run as before: shop_orders_daily, the run about 2026-01-20, dag_run 380. It has two tasks, load_orders and then build_daily_revenue.
You also meet a made-up company with 40 DAGs. Most of them start at midnight. Every number about that company is made up.
This chapter is about Airflow 2. Notes about Airflow 3 are marked. You answer three questions:
Which executors exist, and what does each one need?
How long does a task wait under each one?
How do you choose, and how big must it be?
02 · One machine
Sequential and Local Executors
⏱ 7 min · Topic 2 of 10
The simplest executors run tasks on the scheduler machine. A machine is one computer or one server.
SequentialExecutor runs one task at a time. In Airflow 2 it is the only executor that works with SQLite, the test database from the last chapter. Use it only for learning.
Airflow 3.0 removed SequentialExecutor. There, LocalExecutor also works with SQLite for testing.
LocalExecutor starts each task as a subprocess. A subprocess is a small program that another program starts. Here the scheduler starts it on its own machine.
Several subprocesses run at the same time. The setting parallelism is the limit: how many tasks can run at once. Its default is 32.
Look at the config. Then look at where the two tasks of our run ran:
Both tasks ran on scheduler-1. There is no broker and no worker fleet to deploy.
The host name scheduler-1 is made up. It is the machine that runs the scheduler.
That is the strength of LocalExecutor: nothing else to run. It is also the limit. You cannot add a second machine.
Watch out
Tasks and the scheduler share one machine. A heavy task can slow the scheduler. If the scheduler machine fails, its running tasks fail too.
03 · A fleet of workers
Celery: Workers and a Broker
⏱ 8 min · Topic 3 of 10
CeleryExecutor sends tasks to workers on many machines. Celery is a Python tool for running background jobs.
It needs a broker, a message queue such as Redis or RabbitMQ. It also needs a result backend, a place where workers save results. In many setups this is the metadata database.
Celery workers are long-running. A worker starts once, then waits for tasks and runs many of them. So a task starts fast: our run waited about 2 seconds.
Each worker runs several tasks at once. The setting worker_concurrency says how many. Its default is 16.
You can also split the fleet with queues. A queue is a named line of tasks.
Each worker listens to the queues you give it. Start two workers like this:
Start two workers with different queuesworked example
Shell
# on machine worker-a
airflow celery worker --queues default
# on machine worker-b
airflow celery worker --queues heavy
The names worker-a and worker-b are the hosts from the last chapter. The queue heavy is made up.
Now send a task to a queue. Say build_daily_revenue needs more memory, so it goes to heavy:
This is how the last chapter got one task on worker-a and one on worker-b.
A task with no queue goes to the queue named default.
Watch out
Send a task to a queue that no worker listens to, and it stays queued. After task_queued_timeout, 600 seconds by default, the scheduler retries it or fails it. The error does not name the queue, so check the queue name first.
04 · One pod per task
Kubernetes: One Pod per Task
⏱ 8 min · Topic 4 of 10
KubernetesExecutor starts one new pod for each task. A pod is a small, isolated box that Kubernetes runs. A cluster is the group of machines that runs the pods.
The pod is built from an image: a saved package with your code and libraries. A file called pod_template_file describes the pod. It names the image, the memory and the CPU.
When the task ends, Airflow deletes the pod by default. With no tasks running, no task pods exist.
So no idle workers wait for tasks. You still pay for cluster machines unless the cluster shrinks.
Look at a small template. The image name is made up:
A pod_template_fileworked example
YAML
apiVersion: v1
kind: Pod
metadata:
name: placeholder-name
spec:
restartPolicy: Never
containers:
- name: base
image: my-registry/shop-airflow:1.4
resources:
requests:
cpu: "500m"
memory: "1Gi"
Airflow looks for the container named base. The values 500m and 1Gi mean half a CPU and 1 gibibyte of memory.
Every task gets this pod unless you change it. You change it for one task with executor_config. Here build_daily_revenue asks for more memory:
Each task has its own memory. This is isolation: one task cannot take the memory of another.
The pod for load_orders keeps 1Gi. Only the pod for build_daily_revenue gets 4Gi.
Isolation also covers libraries. Each task can use a different image. Two tasks that need different versions of one library can live in the same DAG.
This is not the same as KubernetesPodOperator. There, the work of one task runs in a container you choose. Running External Compute: Spark, Docker & Kubernetes Pods teaches it.
Watch out
The pod is deleted when the task ends, and its log goes with it. Set up remote logging first: it copies each log to storage outside the pod. A pod that never starts has no task log at all, so read the pod events.
05 · Compare
One Run, Three Ways
⏱ 6 min · Topic 5 of 10
Now run the same two tasks under each executor. A task waits between queued and running. That wait is the cost of the executor.
Each task works for 3 seconds. The waits in the table are made up. Only the order is typical: a process is fastest, a waiting worker is close, and a new pod is slowest.
The run about 2026-01-20 (waits made up)
Executor
load_orders waits
build_daily_revenue waits
Run ends at
LocalExecutor
1 second
1 second
00:00:13
CeleryExecutor
2 seconds
2 seconds
00:00:15
KubernetesExecutor
20 seconds
20 seconds
00:00:51
All times are on 2026-01-21. The run starts at 00:00:00.
Check the Kubernetes row. load_orders is queued at 00:00:02 and runs at 00:00:22. It ends at 00:00:25, after 3 seconds of work.
build_daily_revenue is queued at 00:00:27. It runs at 00:00:47 and ends at 00:00:50. The run ends at 00:00:51.
Only 6 of those 51 seconds are your task code. For one daily run, nobody cares. For thousands of short tasks, the waits add up.
The wait also changes where you look when a task is stuck in queued. Look at the places:
Executor
Look first
Then look at
LocalExecutor
Is the scheduler machine healthy?
CPU and memory on that machine
CeleryExecutor
Is a worker alive on that queue?
The broker
KubernetesExecutor
kubectl get pods: is the pod pending?
Room in the cluster, and the image pull
06 · Decide
How to Choose
⏱ 7 min · Topic 6 of 10
Start from your work, not from a fashion. Four facts decide most cases:
How many tasks run at once.
How long each task runs.
Whether libraries conflict.
What your team can run and fix.
A library conflict means two tasks need different versions of the same library. One shared environment cannot hold both. Look at how the three executors differ:
Question
LocalExecutor
CeleryExecutor
KubernetesExecutor
What do you deploy?
Nothing extra
A broker and workers
A Kubernetes cluster and pod templates
Start-up wait?
Shortest
Short
Longest
How does it grow?
One machine only
Add workers
Add machines to the cluster
Library conflicts?
Hard
Hard, unless you split the fleet
Easy, one image per task
Cost when idle?
The machine
The workers keep running
No task pods
Good for
Small teams and tests
Many short, steady tasks
Long, uneven or conflicting tasks
Short tasks favor Celery, because workers are already running. Long or very different tasks favor Kubernetes, because the start-up wait is small next to the work.
Try one case. A made-up task runs for 3 seconds and waits 20 seconds for its pod. Only 3 of 23 seconds are work, so Celery wins.
Try the other case. A task runs for 40 minutes and needs its own library version. A 20-second wait does not matter, and the isolation does, so Kubernetes wins.
Measure your real task lengths before you decide.
Interview question
Celery or Kubernetes? A strong answer asks questions first: task length, load shape, library conflicts, and team skills. Then it names the trade: start-up wait against isolation.
07 · Scale
Size for the Nightly Peak
⏱ 10 min · Topic 7 of 10
Now the made-up company. It has 40 DAGs with 5 tasks each, so 200 tasks. Each task takes 2 minutes.
Most tasks become ready at midnight. To keep it simple, pretend no task waits for another, so all 200 are ready together. A wave is one group of tasks that run together.
With Celery, capacity is workers times worker_concurrency. Say 4 workers have 16 slots each, so 64 slots. Do 64 tasks run at once?
No. The core setting parallelism is also a limit, and its default is 32.
A pool is another limit: a set of task slots. Look at four setups:
The midnight peak: 200 tasks of 2 minutes (made up)
Setup
Tasks at once
Waves
Time to finish
4 Celery workers, parallelism 32
32
7
14 minutes
4 Celery workers, parallelism 64
64
4
8 minutes
8 Celery workers, parallelism 64
64
4
8 minutes
13 Celery workers, parallelism 200, pool 200
200
1
2 minutes
The number of waves is 200 divided by the tasks at once, rounded up. Tasks use default_pool unless you name another pool. It has 128 slots by default, so row 4 raises it to 200.
Check row 1. 200 divided by 32 is 6.25, so 7 waves. Seven waves of 2 minutes is 14 minutes, and half of the 64 slots stay empty.
Row 3 shows the trap. The extra 4 workers changed nothing, because parallelism was still 64. In row 4, 13 workers give 208 slots, because 200 divided by 16 is 12.5 and you round up.
The last row costs 13 workers all day for a peak that lasts 2 minutes. KubernetesExecutor changes this deal. It could start 200 pods at midnight, if the cluster has room, and then use none.
Parallelism and the pool still apply, so they must allow 200 too.
Add the made-up 20-second pod wait. The peak then takes about 2 minutes 20 seconds, and no task pods run after it.
This needs worker_pods_creation_batch_size raised, because its default of 1 starts one pod per scheduler loop.
Other limits also cap a DAG or a shared system. Concurrency, Pools, Priorities & Deferrable Operators teaches them.
Watch out
Each slot is a process that needs memory. Set worker_concurrency too high, and the machine runs out of memory. The system then kills tasks, and they can appear as zombie tasks.
08 · Modern option
Running More Than One Executor
⏱ 6 min · Topic 8 of 10
Most teams have a main kind of task and a few odd ones. Say 195 small SQL tasks and 5 heavy jobs that need a big pod.
Airflow 2.10 added multiple executors. You list several in the config, and the first one is the default.
A task picks another one with its executor argument.
Look at the config and at our DAG. Here build_daily_revenue is the odd task:
Celery by default, Kubernetes for one taskworked example
The common case stays cheap. Only the odd task gets a pod.
A task with no executor argument uses the first one in the list.
Before 2.10, the only way to mix was a hybrid executor: CeleryKubernetesExecutor or LocalKubernetesExecutor. They sent tasks with one queue name to Kubernetes. From Airflow 2.10, a list of executors does the same job.
Airflow 3.0 does not support them. If you use one, move to a list of executors before you upgrade.
Watch out
Two executors mean two sets of things to run, watch and fix. A healthy Celery fleet tells you nothing about pending pods. Add a second executor only for a real minority of tasks.
09 · Recap
Quick Recap
⏱ 5 min · Topic 9 of 10
An executor is one line of config and a different deployment. Choose it from your tasks, then check the limits above it.
Look at the answers you should know:
Question
Short answer
What does LocalExecutor need?
Nothing extra. Tasks run as subprocesses on the scheduler machine.
What does CeleryExecutor need?
A broker, a result backend and long-running workers.
What does KubernetesExecutor do?
It starts one pod per task, and deletes it at the end.
Capacity of a Celery fleet?
Workers times worker_concurrency, under parallelism.
Default parallelism and worker_concurrency?
32 and 16.
A task stays queued under Celery. First check?
Is a worker alive and listening to that queue?
What replaced the hybrid executors?
A list of executors, from Airflow 2.10.
Try this alone. Your tasks run 10 seconds each, thousands of times a day. Which executor fits, and which number decides it?
The answer is Celery. The start-up wait decides: a made-up pod wait of 20 seconds is longer than the task.
10 · Next Chapter
Next Chapter
You have chosen how tasks are sent out. The next chapter covers how many tasks may run at all.