Airflow Performance: Scheduler Stalls at 2,000 DAGs
Airflow performance tuning for a scheduler stalled at 2,000 DAGs: parsing processes and lag monitoring.
20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.
- ✓Airflow in production with more than 200 DAGs.
- ✓Access to scheduler logs and the metadata database.
- ✓Basic familiarity with Prometheus/Grafana or statsd.
- The scheduler runs one loop: parse DAG files, create runs, dispatch tasks — and parse time dominates everything.
- Three knobs control parsing: parsing_processes, min_file_process_interval, and dagbag caching.
- One file processor per 100-200 DAGs is the rule of thumb; at 2,000 DAGs you need 10 or more.
- Top-level DB calls in DAG files add seconds to every parse — they stalled our scheduler at 2,000 DAGs.
- Monitor scheduler lag (time since last parse) before tuning; tune until lag stays under 60 seconds.
- If lag grows after tuning, redesign: dynamic DAGs, fewer files, or move scheduling off-platform.
The scheduler is a chef with one stove. Every DAG file is a recipe, and the chef must re-read every recipe before he can cook anything. A recipe with a slow step — like calling the bank before cooking — grinds the whole kitchen to a halt. When the menu hit 2,000 recipes, the chef fell so far behind that nobody got a meal.
The scheduler does three jobs in a loop: parse DAG files, create runs for what's due, and hand tasks to the executor. For most teams, step one eats 90% of the time. That's the whole performance story.
At 2,000 DAG files our scheduler stopped scheduling. Not crashed — stopped. The UI was fine, the database was fine, but the parse loop took so long that runs never got created. Every tuning knob we'd ignored for a year was suddenly the only thing that mattered.
We'll walk the parse loop, the three knobs that control it, the database side, and how to monitor scheduler lag. It ends with the honest question: when do you stop tuning and redesign?
Parsing isn't the price of using Airflow. It's the thing you manage.
1. The Scheduler's Loop and Where Time Goes
The scheduler loops forever: pick up DAG files, parse them into a dagbag, decide which runs are due, and hand tasks to the executor. Parse is the step that scales with your codebase.
Each DAG file is imported as Python. The import runs the whole module — including anything at top level. A clean file parses in 100-300ms; a file that opens a DB connection at import time takes seconds.
That difference compounds. With 2,000 files, one bad pattern in fifty files adds minutes to every loop.
Measure the folder before you touch a single knob.
Two loop constants worth knowing: each loop creates at most max_dagruns_to_create_per_loop DagRuns (default 10), and scheduler_idle_sleep_time (default 1s) paces the loop when there's nothing to do.
2. Parsing: The Three Knobs
Knob one: parsing_processes (max_threads in older Airflow 2.x) — how many processes parse DAG files in parallel. Default is 2. At 2,000 DAGs, that's 1,000 files per process per loop.
Knob two: min_file_process_interval — the minimum time between parsing attempts of the same file. Default 30s; too low thrashes the loop, too high delays new DAGs.
Knob three: dagbag caching. Airflow 3 caches parsed DAGs across loops so unchanged files skip re-import — the single biggest win at scale.
Raise processes to match cores, keep the interval at 30-60s, and let the cache do the heavy lifting.
The real ceiling on parsing_processes is memory, not CPU: each process is a full Python interpreter, and while Airflow uses forking and copy-on-write to share it, any import after the fork breaks the sharing and memory climbs. In Airflow 3 both knobs moved to the [dag_processor] section — AIRFLOW__DAG_PROCESSOR__PARSING_PROCESSES — and min_file_process_interval still exists there, default 30s.
3. Database Saturation and Index Health
The scheduler reads the metadata DB on every loop: which runs exist, which tasks are due, which heartbeats are fresh. When the DB slows, the scheduler slows — even with perfect parse times.
Task instance tables grow without bound unless old runs are cleaned. Bloat means dead rows, bigger scans, and index pages that miss the cache.
The fix is boring: index the hot tables, clean old runs, and watch the slowest queries.
4. Monitoring Scheduler Lag
Lag is the gap between now and the last completed parse cycle. Airflow exposes it as dag_processing.last_run.seconds_ago; Prometheus makes it alertable.
Lag under 60 seconds is healthy. Above 120 seconds, runs start firing late. Above 300, you're in the stall zone — runs may never be created at all.
Every tuning change you make should be validated against this single number. Before, during, and after.
5. The 2,000-DAG Failure Story
Month one: 400 DAGs, one parse process, parse times under 200ms each. Nobody checked; nothing broke.
Month nine: 2,000 DAGs. Four legacy files still opened DB connections at import time — each added 8-12 seconds. With a single processor, one loop took 40+ minutes. Runs were due every 5 minutes.
The scheduler was drowning and the queue looked like a backlog, so the first instinct was to raise task parallelism. It changed nothing; the bottleneck was upstream of the queue.
The actual fix took an afternoon: processes to 10, the four files cleaned, caching enabled. Lag dropped from 2,400 seconds to 12.
2,400 to 12. Same hardware.
6. When to Move Scheduling Elsewhere
Tuning has a ceiling. Beyond it, the honest answer is redesign: fewer DAG files via config-driven generation, or fewer DAGs per platform.
If your folder has 5,000 near-identical DAGs, generated-from-config DAGs replace thousands of files with dozens. If a team ships 50 small DAGs a day, a second Airflow instance per domain beats one giant shared one.
And if DAGs are many but tasks are few — hundreds of one-task DAGs — you don't have an Airflow problem. You have an architecture problem.
Know which phase you're in before you spend another hour tuning.
Two scale-out moves change the shape instead of the settings. A standalone DAG processor separates parsing from scheduling — in 2.x opt in with AIRFLOW__SCHEDULER__STANDALONE_DAG_PROCESSOR=True and run airflow dag-processor; in Airflow 3 it's a first-class split that scales independently and keeps the scheduler from executing author code. And the scheduler scales almost linearly, so a second scheduler is a real multiplier — but every additional scheduler multiplies metadata-DB connection traffic, which is why PgBouncer in front of Postgres is the standard companion.
7. A Tuning Checklist for Production
Tune in this order and stop when lag is under 60 seconds.
- Profile the folder — find files parsing over 1 second.
- Fix slow files — top-level DB calls are the usual culprit.
- Raise parsing_processes to match cores.
- Keep min_file_process_interval at 30-60s; rely on caching.
- Check DB bloat and prune old runs.
- Alert on lag and re-run the checklist after every deploy.
And once the folder passes ~1,000 files, set file_parsing_sort_mode to modified_time so recently changed DAGs parse before the long tail of stable ones.
Boring, measurable, reversible. That's what production tuning looks like.
The 2,000-DAG Stall
- An alive scheduler process is not a scheduling scheduler.
- Parse time is the scheduler's real workload; measure it before tuning anything.
- Top-level DB or network calls in DAG files are the classic parse-time killer.
- Tune one knob at a time and verify with the lag metric, never by feel.
| File | Command / Code | Purpose |
|---|---|---|
| profile_parse.py | from airflow.models.dagbag import DagBag | 1. The Scheduler's Loop and Where Time Goes |
| scheduler.env | export AIRFLOW__SCHEDULER__PARSING_PROCESSES=10 | 2. Parsing |
| db_health.sql | SELECT relname, n_live_tup, n_dead_tup, last_vacuum | 3. Database Saturation and Index Health |
| scheduler_lag.yml | groups: | 4. Monitoring Scheduler Lag |
Key takeaways
Common mistakes to avoid
4 patternsMaxing max_active_tasks instead of parse capacity
DB calls at DAG import time
Setting min_file_process_interval too low
Tuning without a lag metric
Interview Questions on This Topic
What does the Airflow scheduler do in a loop?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.
That's Airflow. Mark it forged?
3 min read · try the examples if you haven't