Home DevOps Airflow Executors: Zero Parallelism by Default? Fix It
Advanced 3 min · September 04, 2026
Airflow Executors Explained

Airflow Executors: Zero Parallelism by Default? Fix It

Airflow ran 100 DAGs with zero parallelism on the default executor.

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
September 04, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 30 min
  • You run Airflow past the default install with several DAGs
  • You understand tasks, pools, and basic airflow.cfg settings
  • You can read worker and scheduler logs
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Executors decide where task instances run: Sequential (one at a time), Local (processes on one box), Celery (worker fleet via broker), Kubernetes (one pod per task)
  • Key components: executor backend, parallelism and max_active_tasks caps, pools, and queue routing to worker groups
  • Performance insight: moving 100 DAGs off SequentialExecutor cut queue drain from 4 hours to 12 minutes with parallelism at 32 on one box
  • Production insight: backpressure shows as queued-task age plus saturated pools, so fix pools and DB headroom before adding workers
✦ Definition~90s read
What is Airflow Executors?

An Airflow executor decides where queued task instances actually run, from single-process Sequential to fleet-scale Celery and pod-per-task Kubernetes, bounded by parallelism, per-DAG, pool, and queue caps.

Think of a restaurant kitchen.
Plain-English First

Think of a restaurant kitchen. The Sequential executor is one cook making one dish at a time. Local is three cooks in one kitchen. Celery is a chain of kitchens taking phone orders through a dispatcher. Kubernetes builds a pop-up kitchen for every single order. Each model fits a different size of dinner rush.

The fleet had 100 DAGs and the throughput of one. Tasks queued for hours while CPU graphs stayed flat and engineers blamed the scheduler.

The scheduler was innocent. The executor was Sequential, running one task at a time like a demo laptop. Nobody had changed it since install day.

Executors decide who actually runs your tasks. Pick wrong and no amount of tuning saves you. You'll learn the four options here.

The Executor Job: Who Runs Task Instances

The executor owns one job: turning queued task instances into running processes somewhere. The scheduler decides order; the executor provides the somewhere. Confuse the two and you'll tune the wrong component for weeks. Executor logic runs inside the scheduler process itself, so there is no separate executor daemon to restart; you check it with airflow config get-value core executor.

Sequential runs tasks inline one at a time and exists for dev and tests. Local forks subprocesses on the same host and suits single-node teams. Celery sends tasks through a broker to a fleet of workers. Kubernetes launches a fresh pod per task for full isolation. Remote families split further: queued batch executors (Celery, Batch, Edge) reuse warm workers, while containerized ones (Kubernetes, ECS) spin a clean container per task.

Your executor choice caps everything downstream. No pool or parallelism setting can exceed what the executor physically provides. Since 2.10 you can also mix executors in one deployment (comma-separated list with aliases) and pin tasks via executor='LocalExecutor' or default_args, so steady and spiky work share one control plane.

📊 Production Insight
A team tuned scheduler heartbeats for 2 weeks with zero gain.
The Sequential executor allowed exactly 1 concurrent task.
Rule: confirm AIRFLOW__CORE__EXECUTOR before tuning anything.
🎯 Key Takeaway
Scheduler orders work; executor runs it somewhere real.
Sequential is dev-only; everything else scales differently.
The executor sets the ceiling every other knob lives under.

The Four Executors and Their Ceilings

SequentialExecutor belongs on laptops: zero setup, zero parallelism, SQLite-friendly. LocalExecutor fits teams under roughly 30 DAGs on one sturdy VM: set parallelism to 2x CPU cores and cap per-DAG tasks so one DAG can't flood the box.

CeleryExecutor fits steady fleets with predictable volume. You run a broker plus N workers, scale workers with queue depth, and get second-scale task starts on warm workers. KubernetesExecutor fits bursty or spiky work where tasks need distinct images or resources: each task gets a clean pod, cold start costs 20-60 seconds, and KEDA scales nodes on queue depth.

Choose by shape, not hype. Steady ETL loves Celery's warm workers; nightly ML spikes love Kubernetes' isolation.

config/airflow.cfgYAML
1
2
3
4
5
6
7
8
# airflow.cfg (single-node team, explicit choice)
[core]
executor = LocalExecutor
parallelism = 32
max_active_tasks_per_dag = 8

# verify live config (env overrides file)
# airflow config list | grep -iE 'executor|parallelism|max_active'
📊 Production Insight
Steady ETL on Kubernetes paid 45s pod startup per 2-minute task.
Moving it to Celery cut task overhead 70% overnight.
Rule: warm workers for short tasks, pods for spiky ones.
🎯 Key Takeaway
Sequential for dev, Local for one box, Celery for steady fleets, Kubernetes for bursts.
Cold starts and isolation trade against each other.
Pick by workload shape, not trendiness.

Parallelism vs Max Active Tasks vs Pool

Concurrency applies as a stack and every layer binds. parallelism caps total running tasks instance-wide. max_active_tasks_per_dag caps a single DAG's footprint. Pools cap shared resources like warehouse connections regardless of DAG. Queues route tasks to worker groups with distinct capacity.

Tune bottom-up: pools first to protect external systems, then per-DAG caps to stop one DAG flooding, then global parallelism to match real worker slots. Raising parallelism with a full default_pool changes nothing except your confidence.

Watch the metadata DB as you raise caps. Each running task holds connections and heartbeats; doubling parallelism without DB headroom trades queued tasks for connection timeouts.

📊 Production Insight
parallelism at 64 with default_pool at 16 queued identically to 16.
Raising the pool to 48 drained the backlog in 20 minutes.
Rule: lowest cap wins, so find it first.
🎯 Key Takeaway
Pools protect systems, per-DAG caps protect neighbors, parallelism caps the instance.
Tune pools first, parallelism last.
Every cap you ignore becomes the real ceiling.

Queue Routing That Prevents Starvation

Queues are lane markings. Heavy training tasks on the default queue block quick ETL behind them like a truck in the fast lane. Dedicated queues with matching workers separate traffic by shape.

Tag tasks at authoring time: queue='etl' for warehouse loads, queue='ml' for training, default for lightweight checks. Start workers subscribed to specific queues so capacity follows labels, not hope.

Audit queues quarterly. Labels drift as DAGs evolve, and an ml queue full of ETL tasks is just the default queue with extra steps.

📊 Production Insight
One 2-hour training job blocked 50 ETL tasks daily on default queue.
Splitting ml and etl queues cut ETL p95 wait from 90 to 6 minutes.
Rule: fat jobs never share lanes with quick ones.
🎯 Key Takeaway
Route by task shape, not by team habit.
Dedicated workers per queue keep fat jobs from blocking quick ones.
Audit labels before they drift into fiction.

Choosing an Executor: A Scale Table

Pick with a scale table, not vibes. Under 10 DAGs on one box: LocalExecutor, parallelism 16-32. Ten to fifty DAGs with steady volume: Celery with 2-4 workers and a broker. Bursty ML or strict isolation needs: Kubernetes with requests/limits and KEDA. Beyond that, list two executors in [core] executor (e.g. CeleryExecutor,KubernetesExecutor), alias them for clarity, and route per task with executor='...' so short ETL stays warm while training goes isolated. Metrics then publish per executor (executor.open_slots.<name>), so dashboards split by backend.

Factor in team skill. Celery needs broker ops (Redis/RabbitMQ, Flower, visibility timeout). Kubernetes needs cluster ops (Helm, node pools, pod quotas). The fancier executor you can't operate is worse than the simpler one you can.

Revisit yearly. The executor that fit 20 DAGs rarely fits 120. Retired hybrids (CeleryKubernetesExecutor, LocalKubernetesExecutor) are gone in 3.x; the comma-separated multi-executor config replaces them without the queue-field hack.

dags/nightly_mix.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# celery worker subscribed to its lane
# airflow celery worker -Q etl,default --concurrency 8 --autoscale 12,4

from airflow.sdk import dag, task
from datetime import datetime

@dag(dag_id="nightly_mix", schedule="@daily", catchup=False)
def nightly_mix():
    @task(queue="etl", pool="warehouse_pool")
    def load_warehouse():
        return "heavy load on etl workers"
    @task(queue="default")
    def health_check():
        return "quick check stays unblocked"
    load_warehouse() >> health_check()

nightly_mix()
📊 Production Insight
A 25-DAG team ran Kubernetes for prestige and paid 40% overhead.
Celery cut their infra bill 35% with faster starts.
Rule: match executor to volume, not resume lines.
🎯 Key Takeaway
Scale, burst shape, and ops skill pick the executor.
Revisit the choice as DAG count triples.
Simple and operated beats fancy and fragile.

What Backpressure Looks Like Per Executor

Backpressure looks different per executor. Local shows load average pinned with fork errors. Celery shows broker queue depth climbing while Flower shows idle workers (visibility timeout or queue mismatch). Kubernetes shows pending pods with insufficient CPU or quota errors.

The universal signal is queued-task age: the gap between a task becoming queued and actually running. Ages climbing through peak while capacity looks free mean a cap or route is wrong, not that you need more machines.

Instrument it. Alert when p95 queued age exceeds 10 minutes on prod pools; that page arrives hours before SLA misses do.

💡Idle Workers, Full Queue?
When tasks queue while workers sit idle, suspect pools and queues before the scheduler. Check Admin > Pools occupancy and worker -Q subscriptions first; they explain most phantom backpressure.
📊 Production Insight
Queued age hit 45 minutes while CPU sat at 30%.
A pool cap of 16 was the entire incident.
Rule: alert p95 queued age above 10 minutes.
🎯 Key Takeaway
Queued-task age is the universal backpressure gauge.
Each executor fails in its own dialect; age translates all of them.
Alert on age, not just on failure.
● Production incidentPOST-MORTEMseverity: high

100 DAGs, Zero Parallelism: The Default Executor Surprise. One task at a time stalled the whole fleet.

Symptom
Mornings started fine and afternoons collapsed. Task after task sat queued for hours while the host showed 80% idle CPU. Engineers blamed DAG parsing and scheduler tuning, added retries, and split DAGs apart. Nothing moved the needle because only one task could run at any moment across the entire instance.
Assumption
The team assumed Airflow parallelized out of the box because the UI showed many DAGs side by side. They read queued tasks as scheduler lag and restarted the scheduler twice. Nobody checked which executor was configured because install docs never flagged it as a decision.
Root cause
The install defaulted to SequentialExecutor, which executes exactly one task instance at a time. With 100 DAGs competing for that single slot, queue times grew through the day while CPU stayed idle. Restarts and DAG tweaks couldn't help because the ceiling was architectural: one lane for all traffic.
Fix
They read airflow config list, found SequentialExecutor, and switched the single prod box to LocalExecutor with parallelism set to 32. Heavy DAGs got max_active_tasks_per_dag caps and a dedicated warehouse pool. Queue drain time fell from hours to minutes, and the Celery migration went on the next quarter's roadmap once DAG count passed 40.
Key lesson
  • The default executor is a dev setting; production needs an explicit executor decision.
  • Concurrency is a stack of caps, not one knob, and the lowest cap always wins.
Production debug guideFind the real ceiling when tasks queue while capacity sits idle.4 entries
Symptom · 01
One running task at a time while dozens queue
Fix
Run airflow config get-value core executor and airflow config list | grep -iE 'executor|parallelism'. If it reads SequentialExecutor, that is the ceiling. Switch dev boxes to LocalExecutor and fleets to Celery or Kubernetes, then set parallelism to 2x your worker slots. Multi-executor setups list a comma-separated value here; confirm each named backend appears.
Symptom · 02
Workers idle but tasks stay queued
Fix
Open Admin > Pools and compare occupied slots to totals during peak. If default_pool is full while workers sit idle, create a dedicated pool: airflow pools set etl_pool 16 'ETL warehouse slots'. Assign heavy tasks pool='etl_pool' so they stop starving everything else.
Symptom · 03
Queue grows linearly through the morning peak
Fix
Query queued-task age in the UI Browse > Task Instances filtered to queued, sorted by start_date. Ages above 10 minutes with free workers point at pool or DB caps. Check scheduler logs for pool-deferred messages before adding workers.
Symptom · 04
Long tasks block short tasks on the same queue
Fix
List queue assignments: airflow tasks list <dag_id> shows tasks; cross-check worker startup flags for -Q queues. Move training tasks to queue='ml' with dedicated workers so 2-hour jobs stop blocking 5-minute ETL behind them.
The Four Airflow Executors Compared
ExecutorParallelismInfra costBest for
SequentialExecutor1 task at a timeZero: dev onlySQLite dev and DAG parsing tests
LocalExecutorN processes on one boxOne VMSingle-node teams under ~30 DAGs
CeleryExecutorScales with worker fleetBroker plus worker VMsSteady high-throughput ETL fleets
KubernetesExecutorOne pod per taskCluster plus per-task overheadBursty, isolated, resource-spiky workloads
Multi-executor (2.10+)Per-task routing by executor= paramWarm + isolated in one planeConfig and metrics per backend
Batch / ECS / EdgeCloud-batch or edge workersManaged scale, provider skillNiche burst and edge fleets
⚙ Quick Reference
2 commands from this guide
FileCommand / CodePurpose
configairflow.cfg[core]The Four Executors and Their Ceilings
dagsnightly_mix.pyfrom airflow.sdk import dag, taskChoosing an Executor

Key takeaways

1
SequentialExecutor runs one task at a time; it is never a production choice.
2
LocalExecutor fits single-node teams, Celery fits steady fleets, Kubernetes fits bursty work; 2.10+ lets you mix them per task.
3
parallelism, per-DAG caps, pools, and queues apply together as a stack.
4
Queue routing keeps fat jobs from blocking quick tasks.
5
Backpressure shows as queued-task age; fix pools and DB before adding workers.

Common mistakes to avoid

4 patterns
×

Assuming the default executor provides parallelism

Symptom
100 DAGs queue while the UI shows one running task at a time and CPU sits idle.
Fix
Set AIRFLOW__CORE__EXECUTOR explicitly in env or airflow.cfg and assert it in CI with airflow config list. LocalExecutor suits single-node teams; Celery or Kubernetes for fleets. Never let the default choose for you.
×

Raising parallelism while pools and DB connections stay capped

Symptom
More slots configured but tasks still queue; the bottleneck moved to pools or the metadata DB.
Fix
Tune the full stack: parallelism caps total slots, max_active_tasks_per_dag caps per-DAG pressure, pools cap shared resources. Raise pools first, then parallelism, watching DB connections throughout.
×

Running everything on the default queue

Symptom
One 2-hour model training task blocks 50 five-minute ETL tasks behind it every day.
Fix
Route heavy tasks to dedicated queues with queue='gpu' or queue='etl' and matching workers. Keep the default queue for light tasks so one fat job can't block everything.
×

Treating executor choice as one-time instead of capacity planning

Symptom
Fleet works at 20 DAGs and collapses at 60 with no config change in between.
Fix
Set worker_concurrency and autoscale bounds per queue, and monitor queued-task age. Celery needs broker visibility timeout above your longest task; Kubernetes needs requests/limits so pods don't evict each other.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Why do 100 DAGs show zero parallelism on a fresh install?
Q02SENIOR
Explain the concurrency stack: parallelism vs max_active_tasks vs pools.
Q03SENIOR
Compare Celery and Kubernetes executors on cost, isolation, and cold sta...
Q01 of 03JUNIOR

Why do 100 DAGs show zero parallelism on a fresh install?

ANSWER
SequentialExecutor is the default and runs tasks one at a time. A team seeing UI activity mistook serial execution for concurrency. Switching to LocalExecutor on one box, or Celery for a fleet, plus deliberate parallelism settings restores real concurrency.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What are the four Airflow executors?
02
How do parallelism, max_active_tasks, and pools interact?
03
Celery or Kubernetes executor for my team?
04
What is executor queue routing?
05
What does executor backpressure look like?
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
September 04, 2026
last updated
1,750
articles · all by Naren
🔥

That's Airflow. Mark it forged?

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

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