Home DevOps Airflow Executors: Zero Parallelism by Default? Fix It
Advanced 4 min · August 1, 2026

Airflow Executors: Zero Parallelism by Default? Fix It

Airflow executors run your tasks.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.

Follow
Production
production tested
August 01, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 15-20 min read
  • Airflow installed and at least one DAG running (see the getting-started article).
  • Comfort reading airflow.cfg and environment variables.
  • A basic idea of processes, queues, and worker machines.
  • Familiarity with the scheduler's role in Airflow's architecture.
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • An executor is the component that actually runs task instances: the scheduler decides, the executor executes.
  • SequentialExecutor runs one task at a time; LocalExecutor uses processes on the scheduler host; CeleryExecutor and KubernetesExecutor scale out.
  • The classic default is SequentialExecutor (parallelism=1); Airflow 3.x ships LocalExecutor with parallelism 32 — either way, 100 DAGs can look busy in the UI while tasks serialize; verify with airflow config get-value core executor.
  • parallelism caps tasks per scheduler, max_active_tasks_per_dag caps per DAG, pools cap by priority: three different knobs.
  • Production: CeleryExecutor for steady batch scale, KubernetesExecutor for per-task isolation; queues route work to specific workers.
  • Backpressure speaks one language: a growing "queued" backlog. Watch it before your SLA does.
✦ Definition~90s read
What is Airflow Executors?

An Airflow executor is the component that runs task instances: from a single sequential process, to processes on the scheduler host, to a Celery worker fleet or one Kubernetes pod per task.

Think of Airflow as a restaurant kitchen.
Plain-English First

Think of Airflow as a restaurant kitchen. The scheduler is the chef writing tickets, and the executor is the kitchen staff who actually cook. The default executor is a single cook who can only make one dish at a time, no matter how many tickets pile up. LocalExecutor hires more cooks in the same kitchen. CeleryExecutor hires cooks in remote kitchens connected by a phone line (the broker). KubernetesExecutor rents a private chef in the cloud for every single order.

Every Airflow deployment has one: a scheduler that schedules, and an executor that does the actual work. Most teams never touch the second part, and that's the whole problem.

The default executor is SequentialExecutor. It runs every task instance one at a time, in one process, forever. Your 100 DAGs will parse, your tasks will show "queued" then "running" in the grid view, and everything will look alive. Nothing will be parallel.

Executor choice is a capacity decision, not a config footnote. It decides how many tasks run at once, where they run, and how your fleet fails under load. Get it wrong and you'll be explaining queued backlogs at 2 AM.

Pick the executor for the scale you actually have, then set the concurrency knobs deliberately. The defaults are designed for tutorials, not teams.

1. The Executor Job: Who Actually Runs Task Instances

The scheduler is the brain, but it never executes anything. When a DAG run fires, the scheduler turns each task into a TaskInstance and hands it to the executor. The executor is the arm: it finds capacity and runs the task's operator code.

The metadata DB holds the contract between them. The scheduler writes task instances to the DB; the executor picks them up, marks them running, and reports the outcome. That's why executors can be swapped without touching your DAGs.

Four executors ship with Airflow: SequentialExecutor, LocalExecutor, CeleryExecutor, and KubernetesExecutor. They differ in one question: where does the task process run, and how many can run at once?

Ask that question before picking one. It answers itself at scale.

Architecture note from the docs: the executor is a configuration property of the scheduler and runs inside the scheduler process — which is why changing it is a scheduler restart, never a DAG change.

📊 Production Insight
Scheduler decides, executor executes, DB coordinates.
The DB decouples them, so executors are swappable.
Rule: know who runs your tasks before you scale.
🎯 Key Takeaway
The executor executes TaskInstances on behalf of the scheduler.
Swapping executors never changes your DAG code.
Punchline: capacity lives in the executor, not the DAG.
The Executor Job: Who Runs Task Instances The Executor Job: Who Runs Task Instances The scheduler never executes a task Scheduler Decides what should run writes TaskInstance Metadata DB The contract between them claims + reports Executor Runs the operator code Swap executors, DAG code unchanged UI busy, but one task ran at a time THECODEFORGE.IO
thecodeforge.io
Airflow Executors

2. The Four Executors and Their Ceilings

SequentialExecutor runs one task at a time inside the scheduler process. It exists so you can start Airflow with zero setup. Its ceiling is one task, period.

LocalExecutor spawns separate processes on the scheduler host, bounded by parallelism. It's the right jump from Sequential: real concurrency, no new infrastructure.

CeleryExecutor ships tasks to a Celery worker fleet over a broker. Workers can live anywhere and scale horizontally, each with its own concurrency cap.

KubernetesExecutor creates one pod per task instance. Full isolation, per-task resources, but pod cold starts add seconds to every run.

Set the executor in airflow.cfg, then restart the scheduler. Nothing else changes.

One version check changes the story of this article's incident. "SequentialExecutor by default" is the Airflow 2.x behavior: the 2.x docs configure it by default and warn that some features, sensors for example, don't work properly under it. Airflow 3.x ships LocalExecutor as the configured default, with [core] parallelism set to 32. The tutorial-flavored default changes between majors, so verify what you actually run with airflow config get-value core executor before trusting the grid view.

LocalExecutor has two container gotchas straight from the docs: its worker processes are subprocesses of the scheduler, so a busy parallelism can read as scheduler memory and trigger OOM restarts — size parallelism to the container limit; and the old unlimited parallelism=0 option was removed in Airflow 3.0.0, the value must be greater than zero.

airflow.cfgINI
1
2
3
4
5
6
7
8
9
10
11
[core]
executor = LocalExecutor
parallelism = 8

[scheduler]
max_active_tasks_per_dag = 4

[celery]
# used only when executor = CeleryExecutor
broker_url = redis://redis:6379/0
result_backend = db+postgresql://airflow:airflow@postgres:5432/airflow
Output
executor = LocalExecutor
parallelism = 8
⚠ The Default Is a Tutorial Setting
SequentialExecutor exists so a fresh install works with zero setup. Shipping it to production means every DAG run serializes behind one task slot, and the grid view will not tell you.
📊 Production Insight
Sequential: 1 slot. Local: host-bound. Celery: fleet-bound.
Kubernetes: per-task isolation with cold-start cost.
Rule: pick the ceiling that matches your workload, then tune below it.
🎯 Key Takeaway
Each executor has a hard ceiling on parallelism.
Sequential's ceiling is one; everything else scales differently.
Punchline: choose the ceiling, then tune the slots under it.
The Four Executors and Their Ceilings The Four Executors and Their Ceilings Where tasks run decides how many run at once SequentialExecutor 1 task at a time · in one process LocalExecutor Processes on host · parallelism bound CeleryExecutor Worker fleet via broker · scales out KubernetesExecutor One pod per task · full isolation Default executor = zero parallelism Fix: LocalExecutor → CeleryExecutor THECODEFORGE.IO
thecodeforge.io
Airflow Executors

3. The Concurrency Stack: parallelism, max_active_tasks_per_dag, Pools

Three knobs control concurrency, and they stack. parallelism is the global cap: how many task instances the scheduler will run at once across the whole deployment.

max_active_tasks_per_dag caps how many tasks of one DAG run simultaneously. A fan-out of 50 mapped tasks can still respect a cap of 6.

Pools cap concurrency on a logical resource: the database writer, the API rate limit, the GPU queue. Tasks declare pool="db_writer" and wait their turn regardless of executor.

The effective ceiling is the smallest gate you hit. The graph view shows states; the queue shows pressure. When tasks wait, find which gate they're stuck behind.

Set all three deliberately. Defaults hide contention until it bites.

market_etl.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from airflow.decorators import dag, task
from datetime import datetime

@dag(
    dag_id="market_etl",
    schedule="@daily",
    start_date=datetime(2026, 1, 1),
    catchup=False,
    max_active_tasks=6,
    max_active_runs=1,
    tags=["finance", "etl"],
)
def market_etl():
    @task
    def extract_stock_data(symbol: str) -> str:
        return f"extracted:{symbol}"

    @task
    def load_to_warehouse(payload: str) -> str:
        return f"loaded:{payload}"

    for symbol in ["AAPL", "MSFT", "NVDA"]:
        load_to_warehouse(extract_stock_data(symbol))

market_etl()
Output
6 concurrent tasks per DAG run, 1 DAG run at a time
Mental Model
Three Gates, One Queue
Every queued task is stuck at one of three gates
  • parallelism: the fleet-wide gate, all executors share it.
  • max_active_tasks_per_dag: the per-DAG gate, fans stay tamed.
  • pools: the per-resource gate, priority_weight decides who crosses first.
📊 Production Insight
Smallest knob wins: the effective cap is the tightest gate.
Queue depth tells you which gate is jammed.
Rule: tune all three, or you're guessing at contention.
🎯 Key Takeaway
parallelism, per-DAG caps, and pools stack multiplicatively.
Queued tasks always sit behind the narrowest gate.
Punchline: the smallest setting you forget is the one that throttles you.
The Concurrency Stack: Three Gates, One Queue Concurrency: Three Gates, One Queue parallelism · max_active_tasks_per_dag · pools Queued task Waits at a gate parallelism Fleet-wide cap across the deployment max_active_tasks_per_dag Per-DAG cap · tames fan-outs pools Per-resource cap · pool=db_writer Runs on executor Smallest gate decides Tasks wait at the narrowest gate THECODEFORGE.IO
thecodeforge.io
Airflow Executors

4. Queue Routing: Which Worker Takes This Task

Once an executor can scale, you need a way to send work to the right place. That's the queue attribute on a task.

A Celery worker starts with --queues=default,heavy_ml. A task with queue="heavy_ml" only runs on workers listening for it. Same idea on KubernetesExecutor: task queues map to pod configurations, so a GPU job can schedule onto GPU-capable pods.

This is how teams separate fast batch from long ML runs without copying DAGs. It's also the quietest failure mode in Airflow: a task queued to a queue nobody listens on waits forever and nobody gets paged.

Audit your queues when tasks idle. The mapping is the contract.

model_training.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from airflow.operators.python import PythonOperator


def train_forecast_model():
    # GPU-bound training: only runs on heavy_ml workers
    pass


train_model = PythonOperator(
    task_id="train_forecast_model",
    python_callable=train_forecast_model,
    queue="heavy_ml",
    dag=dag,
)
Output
Task routed to workers listening on queue heavy_ml
📊 Production Insight
Queues route work to specific workers or pod configs.
A task waits forever if no worker listens on its queue.
Rule: grep queue names across workers before you trust routing.
🎯 Key Takeaway
queue steers a task to the workers that can run it.
Mismatched queues strand tasks silently.
Punchline: every queue name needs a matching listener somewhere.

5. Choosing an Executor: A Scale Table

Here's the honest scale table. One machine, one dev: SequentialExecutor is fine and even convenient. A single machine running a real daily batch: LocalExecutor with parallelism tuned to your cores.

Two or more machines, or steady nightly volume: CeleryExecutor with a Redis or RabbitMQ broker and workers sized by memory. Teams that already run Kubernetes, or need per-task isolation and clean scaling: KubernetesExecutor.

Mixed workloads are normal. You can run CeleryExecutor for the steady batch and route heavy tasks to Kubernetes pods. Both under one scheduler, one metadata DB.

The wrong pick costs you either idle capacity or constant queue pressure. Revisit it when your task volume changes by 10x, not when it doubles.

Since Airflow 2.10.0 there's a first-class way to mix executors without queue tricks: configure several comma-separated in [core] executor and assign tasks with the executor parameter (or per DAG through default_args). The first entry is the environment default; name an executor that isn't in the config and the DAG fails to parse with a UI warning. The old statically-coded hybrids (LocalKubernetesExecutor, CeleryKubernetesExecutor) were removed in Airflow 3.0.0 — the multi-executor config is their replacement.

📊 Production Insight
Scale table: dev=Sequential, single host=Local, fleet=Celery, isolation=Kubernetes.
Mixed setups combine executors via queue routing or multi-executor config (2.10.0+).
Rule: revisit the choice at 10x volume, not 2x.
🎯 Key Takeaway
Executor choice follows deployment size and isolation needs.
You can mix Celery and Kubernetes under one scheduler.
Punchline: the right executor is the one sized to your fleet, not your ego.
Which executor should I run?
IfSingle machine, development or demos
UseSequentialExecutor or LocalExecutor with parallelism=2
IfOne host running a real daily batch
UseLocalExecutor with parallelism tuned to CPU cores
IfMultiple machines or steady nightly volume
UseCeleryExecutor with autoscaled workers
IfPer-task isolation, varied resource needs
UseKubernetesExecutor: one pod per task instance
IfSteady batch plus heavy isolated jobs
UseCeleryExecutor with heavy jobs routed to Kubernetes pods

6. Backpressure: What Each Executor Looks Like When It Fails

Every executor has a signature failure. Sequential and Local fail as host exhaustion: memory pressure, slow scheduler, tasks queuing behind each other with no obvious culprit.

Celery fails as a silent backlog: workers dead, broker alive, tasks queued for hours with zero alerting. The UI shows queued, the calendar fills with late runs.

Kubernetes fails as eviction chaos: no resource limits, pods get OOM-killed, kubelet evicts them, and tasks fail at startup or crash mid-run.

Backpressure looks the same everywhere, though: queued tasks with age. Watch queued-task age, not the grid view. It's the only signal all four executors share.

Set an alert on it before your customers do.

📊 Production Insight
Each executor fails differently but signals the same way.
Queued-task age is the universal backpressure metric.
Rule: alert on queued age, not on task states.
🎯 Key Takeaway
Sequential and Local die by host exhaustion; Celery by silent backlog; Kubernetes by evictions.
Queued-task age catches all of them.
Punchline: backpressure never hides if you measure queue age.
● Production incidentPOST-MORTEMseverity: high

100 DAGs, Zero Parallelism

Symptom
The platform grew to 100 DAGs and roughly 900 tasks a day, yet every run finished hours late. The UI showed tasks moving through queued and running states, but total throughput never exceeded one task at a time.
Assumption
The team assumed the green "running" boxes in the grid view proved real concurrency. Nobody had touched executor configuration, so the install kept the default: SequentialExecutor.
Root cause
Airflow's default executor is SequentialExecutor, which runs every task instance one at a time in a single process. No DAG-level setting overrides it. The team misread UI activity as concurrency while the whole fleet serialized behind one task slot.
Fix
Moved the deployment to LocalExecutor with parallelism tuned to the host's cores, then to CeleryExecutor with two workers as the DAG count grew. Set max_active_tasks_per_dag per workload and verified true parallelism against the queued-task backlog instead of the grid view.
Key lesson
  • The default executor is SequentialExecutor: one task at a time, no exceptions.
  • Green task boxes in the UI do not mean parallel execution.
  • Executor choice is a capacity decision, not a tuning afterthought.
  • Verify concurrency with parallelism settings and queue depth, never the grid view.
  • Version check: SequentialExecutor is the Airflow 2.x default; Airflow 3.x ships LocalExecutor with parallelism 32 — on any major, confirm with airflow config get-value core executor.
Production debug guideSymptom to Action5 entries
Symptom · 01
Tasks pile up in queued for minutes
Fix
Read the executor config with airflow config get-value core executor. SequentialExecutor serializes by design; a distributed setup needs Celery or Kubernetes.
Symptom · 02
Only one task runs at a time despite many DAGs
Fix
Raise parallelism, then check max_active_tasks_per_dag and pool caps. The bottleneck is whichever knob is smallest.
Symptom · 03
Tasks never leave the queue on Celery
Fix
Check the broker connection, worker count, and flower. A dead broker or zero workers strands every queued task silently.
Symptom · 04
Tasks run on the wrong machines
Fix
Compare task queue attributes with worker queues. A task queued to heavy_ml will wait forever if no worker listens on that queue.
Symptom · 05
Scheduler host is saturated under load
Fix
LocalExecutor runs tasks on the scheduler host. Offload with Celery workers or Kubernetes pods before the host becomes the bottleneck.
★ Quick Debug Cheat SheetCommon executor issues and immediate actions
Everything queues, nothing runs in parallel
Immediate action
Read the active executor
Commands
airflow config get-value core executor
airflow config get-value core parallelism
Fix now
Switch to LocalExecutor or CeleryExecutor and restart the scheduler.
One DAG throttles the whole fleet+
Immediate action
Check per-DAG and pool caps
Commands
airflow config get-value scheduler max_active_tasks_per_dag
airflow pools list
Fix now
Raise max_active_tasks_per_dag or move heavy tasks to a bigger pool.
Tasks stuck queued on a distributed setup+
Immediate action
Verify workers and broker
Commands
airflow celery flower
airflow config get-value celery broker_url
Fix now
Restart workers or fix the broker connection; check queue depth after.
The Four Executors at a Glance
ExecutorRuns tasksParallelism ceilingBest forFailure signature
SequentialExecutorIn the scheduler process1 task at a timeDev, demos, tutorialsEverything serializes silently
LocalExecutorProcesses on the scheduler hostparallelism (host-bound)Single-host batch workloadsHost CPU/memory saturation
CeleryExecutorCelery worker fleet via brokerWorkers x concurrencyMulti-machine, steady nightly volumeDead workers, silent queue backlog
KubernetesExecutorOne pod per task instanceCluster capacityPer-task isolation, heterogeneous workloadsOOM kills and evictions
⚙ Quick Reference
3 commands from this guide
FileCommand / CodePurpose
airflow.cfg[core]2. The Four Executors and Their Ceilings
market_etl.pyfrom airflow.decorators import dag, task3. The Concurrency Stack
model_training.pyfrom airflow.operators.python import PythonOperator4. Queue Routing

Key takeaways

1
The executor, not the scheduler, actually runs task instances.
2
SequentialExecutor defaults to zero parallelism; production needs Local, Celery, or Kubernetes.
3
parallelism, max_active_tasks_per_dag, and pools stack
the smallest gate throttles you.
4
Queue attributes route tasks to specific workers or pod configurations.
5
Queued-task age is the universal backpressure signal across every executor.

Common mistakes to avoid

4 patterns
×

Shipping SequentialExecutor to production because the UI shows activity.

Symptom
Every task run serializes; runs finish hours late with no obvious cause.
Fix
Move to LocalExecutor or CeleryExecutor and set parallelism deliberately, then verify with queue depth.
×

Raising parallelism but never touching max_active_tasks_per_dag.

Symptom
One fan-out DAG hogs every slot while other DAGs starve.
Fix
Tune both knobs per workload: fleet-wide cap plus a sane per-DAG cap.
×

Forgetting that pools cap across all DAGs.

Symptom
High-priority DAGs wait behind a pool hog with no timeout.
Fix
Right-size pools and use priority_weight so critical tasks cross first.
×

Assuming every worker listens on every queue.

Symptom
Tasks sit queued while idle workers on other queues do nothing.
Fix
Match task queue attributes to worker --queues lists and audit with flower.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does the executor do in an Airflow deployment?
Q02SENIOR
Compare Sequential, Local, Celery, and Kubernetes executors. When would ...
Q03SENIOR
Tasks are queued for 30 minutes on a CeleryExecutor deployment. Walk thr...
Q01 of 03JUNIOR

What does the executor do in an Airflow deployment?

ANSWER
The scheduler creates TaskInstances and the executor executes them. It picks up ready task instances, runs the operator code, and reports results. Executors differ in where tasks run and how many run concurrently.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What executor does Airflow use by default?
02
What's the difference between parallelism and max_active_tasks_per_dag?
03
Do I need Kubernetes to run Airflow in production?
04
Can I change the executor without touching my DAGs?
05
Can I run more than one executor at once?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
August 01, 2026
last updated
1,750
articles · all by Naren
🔥

That's Airflow. Mark it forged?

4 min read · try the examples if you haven't

Previous
Airflow Datasets and Assets
21 / 37 · Airflow
Next
Airflow Docker Compose Setup