skip to content

In Airflow, how do LocalExecutor, CeleryExecutor and KubernetesExecutor differ in where tasks run?

level: middleimportance: must knowfreq 80%

answer

  1. it lives inside the scheduler, not beside it
  2. one machine, many machines, or one pod each
  3. one of them needs a broker
  4. one of them creates and destroys a pod per task

basics

~10 s

LocalExecutor runs tasks as subprocesses on the scheduler's machine, CeleryExecutor sends them through a broker to separate long-lived worker processes on other machines, and KubernetesExecutor creates one short-lived pod per task instance.

solid answer

~50 s

The executor is the pluggable piece inside the Airflow scheduler that decides how a queued task instance becomes a running process. **`LocalExecutor`** forks subprocesses on the scheduler host — simple, no extra infrastructure, but capped by that one machine. **`CeleryExecutor`** publishes the task to a broker (Redis or RabbitMQ) and long-running `airflow celery worker` processes pull from it; you scale horizontally by adding workers, route work with queues, and pay for a broker plus permanently-on worker capacity. **`KubernetesExecutor`** asks the Kubernetes API for one worker pod per task instance, which runs `airflow tasks run` and exits — perfect isolation and per-task images and resources, at the cost of pod startup latency on every task. `SequentialExecutor` (Airflow 2 only, removed in Airflow 3) runs one task at a time and exists purely for SQLite-backed local play. Airflow 2.10+ also lets you configure several executors at once and pick one per task.

code

text · 7 lines
text
[core]
executor = CeleryExecutor
parallelism = 32

[celery]
broker_url = redis://redis:6379/0
result_backend = db+postgresql://airflow@db/airflow

go deeper

for a junior

Recall the names and the one-line difference: same machine, separate worker machines via a broker, or one pod per task on Kubernetes.

for a middle

Explain that the executor runs inside the scheduler, and describe what infrastructure each option adds — broker and workers versus Kubernetes API access and pod templates.

for a senior

Discuss the operational consequences: code and dependency distribution to workers, pod startup latency, remote logging, and how each option fails under load.

for a principal

Be able to justify a platform choice on cost, isolation and team capability, including hybrid setups where short tasks and heavy tasks take different execution paths.

## What an executor actually is A common misconception is that the executor is a service you deploy. It is not. The executor is a Python class instantiated **inside the scheduler process**, chosen with `[core] executor` in `airflow.cfg` (or `AIRFLOW__CORE__EXECUTOR`). When the scheduler decides a task instance may run, it puts it in the `queued` state and hands it to the executor, whose only job is: get this task instance executed somewhere and report back what happened. Everything else — dependency logic, retries, state — belongs to the scheduler and the metadata database and is identical whichever executor you pick. That is why changing executors does not change your DAG code. ## SequentialExecutor One task at a time, in the scheduler process, and the only executor that works with a SQLite metadata database. It is the default in a fresh Airflow 2 install and exists so `airflow standalone` works on a laptop. It is not a production option, and Airflow 3 removed it entirely. ## LocalExecutor `LocalExecutor` forks a subprocess per task instance on the machine running the scheduler. It needs a real database (PostgreSQL or MySQL) but no broker, no cluster, no extra moving parts. Its ceiling is that machine's CPU and memory, bounded by `[core] parallelism`. For a single team with tens of DAGs and modest tasks — especially tasks that mostly submit work elsewhere, like a `BigQueryInsertJobOperator` or a `dbt` invocation — it is a perfectly serious production choice and the one that fails in the fewest ways. Its weaknesses are real: the scheduler host is also the execution host, so a memory-hungry task can starve scheduling; you cannot give one task a different Python environment; and scaling means a bigger machine. ## CeleryExecutor `CeleryExecutor` turns execution into a distributed queue. The executor publishes a message to a **broker** (`[celery] broker_url` — Redis or RabbitMQ), and `airflow celery worker` processes running elsewhere consume it. Each worker runs several tasks concurrently (`[celery] worker_concurrency`), and total capacity is roughly workers × concurrency, still bounded by `[core] parallelism` per scheduler. Two properties matter operationally. First, **queues**: a task can set `queue="gpu"` and you start workers with `airflow celery worker --queues gpu`, which routes heavy or specialised work to the right machines. Second, **code distribution**: every worker must have the same DAG files and the same Python dependencies as the scheduler, usually via a baked image or git-sync. Version skew between scheduler and worker is the classic Celery deployment bug. The costs are a broker to operate and monitor, and workers that are running (and billed) whether or not there is work — though autoscaling on queue depth, e.g. with KEDA, softens that. ## KubernetesExecutor `KubernetesExecutor` creates **one pod per task instance** through the Kubernetes API. The pod runs `airflow tasks run` for that task, writes its state, and terminates. There are no idle workers: capacity is whatever the cluster can schedule. The big win is isolation and heterogeneity. Each task can have its own image, resource requests, node selector or tolerations, either from a pod template file or per task with `executor_config={"pod_override": k8s.V1Pod(...)}`. One task can ask for 16 GiB and a GPU node while its neighbour asks for 200 MiB. A task that leaks memory takes down only its own pod. The big cost is per-task overhead: scheduling a pod, pulling an image and booting a Python process takes seconds, which is fine for a ten-minute Spark submission and terrible for four hundred two-second tasks. You also need remote logging, because pod-local logs disappear with the pod. ## Hybrids `CeleryKubernetesExecutor` and `LocalKubernetesExecutor` let short tasks take the cheap path and heavy ones get their own pod. From Airflow 2.10, `[core] executor` accepts a comma-separated list of executors and a task can name which one it wants, which is the direction the project has taken. ## Choosing Start with `LocalExecutor` and move only when a real constraint appears: horizontal scale or queue routing pushes you to Celery; per-task isolation, wildly heterogeneous resources or a strong existing Kubernetes platform push you to Kubernetes.

  • Does switching from LocalExecutor to CeleryExecutor require changing DAG code?
    No. The executor only decides how a queued task instance gets run; dependencies, retries, XComs and state all live in the scheduler and metadata database and behave identically. What changes is deployment: you now need a broker, worker processes, and a guarantee that every worker has the same DAG files and Python dependencies as the scheduler. Tasks that relied on scheduler-local files or state will break, because they now run elsewhere.
  • With CeleryExecutor, how do you make sure a heavy task runs on a specific set of machines?
    Give the task `queue="heavy"` and start the machines you want with `airflow celery worker --queues heavy`. Only workers subscribed to that queue will consume it. The trap is a typo or a decommissioned worker pool: if no worker consumes the queue, the task sits in `queued` indefinitely with no error, because Celery routing is a subscription, not a validated assignment.
  • Why does KubernetesExecutor make remote logging effectively mandatory?
    Each task runs in a pod that is deleted once it finishes, taking its local log files with it. Without remote logging to S3, GCS or Azure Blob configured, the web UI can only fetch logs from a pod that no longer exists, so successful and failed runs alike show empty logs minutes later.

saying these in an interview costs you the question

  • Calls the executor a separate service you deploy alongside the scheduler
  • Says LocalExecutor cannot be used in production under any circumstances
  • Thinks KubernetesExecutor keeps a pool of warm worker pods
  • Believes switching executors requires rewriting DAGs
  • Confuses Airflow's Celery queues with an Airflow pool

context