Home DevOps Airflow High Availability: The Scheduler That Double-Ran
Advanced 6 min · August 1, 2026

Airflow High Availability: The Scheduler That Double-Ran

Airflow HA can double-run every DAG when schedulers overlap.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Production
production tested
August 01, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 15-20 min read
  • Airflow deployed with a Postgres metadata database.
  • Comfort running airflow CLI commands and SQL against the metadata DB.
  • A basic idea of what the scheduler does in the grid view.
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Airflow HA means running multiple schedulers against one shared metadata database — the DB stays the single source of truth.
  • Key components: scheduler heartbeats, the DAG processor pool, ha_interval election tuning, and health endpoints.
  • Two schedulers double-queued our task instances — every DAG ran twice until we tuned ha_interval and concurrency.
  • Schedulers coordinate through the metadata DB, so database latency sets the ceiling on how many schedulers you can run.
  • Production rule: alert on scheduler heartbeat age and treat a second scheduler as a backup, not a second brain.
  • Databases: PostgreSQL 12+ and MySQL 8.0+ run any number of schedulers out of the box; MariaDB below 10.6 is unsupported and SQL Server is untested.
✦ Definition~90s read
What is Airflow High Availability?

Airflow high availability is running multiple active schedulers that coordinate through one shared metadata database, governed by heartbeats, leases, and tuned ha_interval settings — monitored so a dead scheduler is replaced, never duplicated.

Imagine two alarm clocks in one house.
Plain-English First

Imagine two alarm clocks in one house. If both go off for the same wake-up, you get two mornings. Airflow schedulers are alarm clocks that claim jobs from the same notebook (the metadata database), and normally they take turns politely. Our team added a second scheduler for safety — but nobody set the turn-taking rules, so both claimed the same DAG runs and every task ran twice. The fix wasn't fewer clocks; it's teaching the clocks to coordinate and watching the heartbeat to prove only one is leading at a time.

High availability is the first thing teams ask for and the last thing they configure. Add a second scheduler, call it HA, and go home. Except Airflow schedulers don't sit idle in standby — they all claim work from the same metadata DB, and nothing stops two of them from claiming the same task. That's how our fleet started double-running every DAG.

The honest framing: Airflow HA is not two brains deciding. It's one shared database with several workers that politely coordinate through it. The database is the brain; the schedulers are hands. Lose that framing and every HA setup becomes a double-run machine.

The coordination is real and mostly automatic — leases, heartbeats, DAG processor pools. But "mostly automatic" is where incidents live: the knobs that govern it are config keys most teams never open, and the failure mode is duplicated data, not a crash.

You don't need HA until you do. When you do, you need it boring.

This is how you make it boring.

1. What HA Actually Means for Airflow

High availability for Airflow is not "two schedulers, one leads." Every scheduler you start is active — each one claims DAG runs, queues task instances, and parses DAG files. What keeps that from descending into chaos is the metadata database.

The metadata DB is the coordination point: schedulers write heartbeats, claim leases, and read state from it. The database is the brain; schedulers are hands doing the same job. The architecture is safe because of how the hands coordinate, not because anything stands idle.

So HA has three moving parts: multiple scheduler processes, a shared metadata DB that can handle them, and the coordination knobs that stop them from stepping on each other. Skip any one and you have HA in name only.

The coordination is deliberately database-only: no Raft or Paxos, no ZooKeeper or Consul. Schedulers serialize their claim-and-enqueue step with row-level locks on the Pool table (SELECT ... FOR UPDATE), which is how two schedulers respect the same concurrency limits.

That's the mental model everything else in this article builds on.

Mental Model
The DB Is the Brain
Think of the scheduler fleet as several baristas and the metadata DB as the order book.
  • Every scheduler actively claims work — none are idle standbys.
  • Heartbeats prove liveness; leases coordinate claims.
  • The DB stores the state that makes tasks run once.
  • HA protects against scheduler death, not misconfiguration.
📊 Production Insight
HA means multiple active schedulers around one DB.
The DB is the brain; schedulers are interchangeable hands.
Rule: design for coordination, not for standby.
🎯 Key Takeaway
More schedulers = more coordination, not more reliability.
The metadata DB is the single source of truth in any HA setup.
Punchline: HA is a database story wearing a scheduler costume.
airflow-singleton-ha-diagram2 HA: One Brain, Many Hands The DB coordinates; schedulers act Scheduler 1 active — claims and queues Scheduler 2 active — claims and queues Metadata Database heartbeats · leases · shared state No idle standby — all schedulers act HA guards death, not confusion The DB is the brain schedulers are interchangeable hands THECODEFORGE.IO
thecodeforge.io
Airflow Singleton Ha

2. Multiple Schedulers: Safe With What Caveats

Multiple schedulers are safe — Airflow designed for it — but the safety is conditional. The conditions are version support, a Postgres-grade metadata DB, and coordination knobs set deliberately instead of left at defaults.

The first condition is mundane: only recent Airflow 2.x and 3.x support multiple schedulers properly. On older versions the scheduler was assumed to be a singleton, and the feature is the coordination layer, not the process count. Check your version before you scale.

The second is the database. Schedulers coordinate through it, which means the DB must handle roughly twice the queries and hold locks gracefully. SQLite — fine for a laptop — is a dead end for HA. Postgres with adequate pool size is the floor.

The third condition is where incidents live: ha_interval and related tuning. Leave them at defaults and the coordination is sloppy; set them blind and you break failover. Understand the knob before you turn it.

The docs spell out the database floor precisely: PostgreSQL 12+ and MySQL 8.0+ run any number of schedulers "all ready to go" — no further setup needed — while MariaDB below 10.6 is not supported (it lacks the SKIP LOCKED/NOWAIT clauses) and SQL Server is untested. One lock-related flag matters: use_row_level_locking must stay True — the docs say that with it False, you should not run more than one scheduler at once.

One executor caveat: multi-scheduler HA is built for Celery or Kubernetes executors, where tasks run independently of the scheduler. Running LocalExecutor on each scheduler ties its tasks to its process — restart a scheduler and its tasks die — which quietly defeats the HA you thought you had.

scheduler_env.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
# Coordination knobs on BOTH schedulers — identical values
AIRFLOW__SCHEDULER__HA_INTERVAL=30
AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC=5
AIRFLOW__SCHEDULER__PARSING_PROCESSES=4
AIRFLOW__SCHEDULER__MIN_FILE_PROCESS_INTERVAL=30

# Per-DAG concurrency cap — the duplicate-execution safety net
AIRFLOW__CORE__MAX_ACTIVE_TASKS_PER_DAG=8

# Verify both schedulers see the same values:
airflow config get-value scheduler ha_interval
airflow config get-value scheduler scheduler_heartbeat_sec
📊 Production Insight
Multi-scheduler safety has three preconditions.
Version support, a real Postgres, and tuned coordination.
Rule: verify all three before scaling scheduler count.
🎯 Key Takeaway
HA schedulers need an HA-capable database first.
Default coordination settings work until they quietly don't.
Punchline: the DB and the version gate everything else.
airflow-singleton-ha-diagram3 Multi-Scheduler Safety: Three Gates All three before you scale 1. Supported Airflow version recent 2.x or 3.x only 2. Postgres-grade metadata DB SQLite is a dead end 3. Coordination tuned on purpose ha_interval and heartbeat settings Safe — all three met multi-scheduler HA works HA in name only skip one and it quietly breaks THECODEFORGE.IO
thecodeforge.io
Airflow Singleton Ha

3. The DAG Processor Pool

DAG parsing is the scheduler's most expensive habit, and it's the one that bit us. Every scheduler runs a DAG processor pool that parses the DAG folder into a snapshot the scheduler then uses to create runs and queue tasks.

With one scheduler, the pool is simple. With two, you have two pools parsing the same files into overlapping snapshots — and stale snapshots are where double-scheduling starts. The coordination that prevents this is part of the scheduler's lease and heartbeat machinery, and it works — until versions drift or the folder is large enough that parsing lags.

The practical discipline: one DAG source shared by all schedulers (the same volume or the same git-sync sidecar), identical parsing configuration on every scheduler, and a min_file_process_interval high enough that the pools aren't stampeding the same files every loop.

Parse slowness doesn't look like an HA problem. It looks like stale DAGs, missed runs, and the occasional duplicate. Treat parse health as HA health.

📊 Production Insight
Two schedulers mean two DAG processor pools.
Stale or overlapping snapshots breed duplicates.
Rule: one shared DAG source, identical parse config.
🎯 Key Takeaway
The DAG processor pool feeds the scheduler's decisions.
Overlapping pools without coordination double-queue tasks.
Punchline: parse health is HA health — monitor both.

4. Database HA: Read Replicas and Failover

You made the database the brain — so the database itself needs redundancy. Airflow's metadata DB is one writer, and that writer needs a real failover story: managed Postgres with automatic failover, or replication configured so a standby can take over the write role.

Read replicas are tempting for HA, and mostly wrong here. Airflow's schedulers need consistent reads of the state they're about to claim. A read replica with replication lag lets two schedulers both decide they own the same DAG run — the exact double-run we lived through.

The safe pattern: write to one primary, and let Airflow's own retries absorb brief failover windows. If you add replicas for reporting queries, never point scheduler config at them. The scheduler reads what it writes.

Failover for the database is the slowest part of any HA story — plan for minutes, not seconds — and it's the part most runbooks forget to rehearse.

db_ha_checks.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
-- Who is claiming what? Duplicate claims are the HA tell.
SELECT dag_id, run_id, task_id, COUNT(*) AS claims
FROM task_instance
GROUP BY 1, 2, 3
HAVING COUNT(*) > 1;

-- Are both schedulers alive and heartbeating?
SELECT process_uuid, last_heartbeat, is_active
FROM scheduler_uptime
ORDER BY last_heartbeat DESC;

-- Replication lag: must stay near zero for scheduler reads
SELECT now() - pg_last_xact_replay_timestamp() AS replica_lag;
📊 Production Insight
The metadata DB needs its own failover story.
Read replicas with lag breed duplicate scheduler claims.
Rule: schedulers read from the primary; replicas serve reports.
🎯 Key Takeaway
One-writer databases need planned failover, not replicas.
Lag between claim and read is the double-run mechanism.
Punchline: your HA is only as good as your DB failover rehearsal.

5. Detecting Double-Scheduling

The tell for double-scheduling is in the metadata DB before it's in your data warehouse: two task_instance rows for the same dag_run, or a run that claims more task instances than the DAG defines. The grid view shows it as duplicate runs; the SQL shows it as duplicate rows.

But detection shouldn't wait for the duplicate. Two earlier signals precede it: leadership churn and stale heartbeats. If schedulers keep trading the lead, or one scheduler's heartbeat age grows while another shrinks, the coordination is failing before the tasks are.

Then there's the operational signal: task age. When two schedulers queue the same work, the queue behaves oddly — tasks complete in bursts and the queue depth never matches the runbook. Alert on the difference between DAG runs triggered and runs completed.

Detection is a query you can run today, plus two metrics you should alert on tomorrow. Build both before you need either.

What happens next is adoption, not restart: when a scheduler is detected dead, its running and queued tasks are "adopted" by the survivors — the sweep is governed by orphaned_tasks_check_interval and the liveness threshold scheduler_health_check_threshold. And the CLI ships an HA-ready liveness check: airflow jobs check --job-type SchedulerJob --allow-multiple --limit 100 succeeds as long as at least one scheduler is alive.

detect_double_schedule.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1. Duplicate task instances — the smoking gun
airflow tasks states market_etl load_orders 2026-07-15

# 2. Runs claiming more tasks than the DAG has
SELECT dag_id, run_id, COUNT(*) AS task_count
FROM task_instance
GROUP BY 1, 2
HAVING COUNT(*) > 6;  -- our DAG defines 6 tasks

# 3. Heartbeat age per scheduler — leader vs laggard
SELECT process_uuid, now() - last_heartbeat AS age
FROM scheduler_uptime;

# 4. The config knob that governs claim timing
airflow config get-value scheduler ha_interval
📊 Production Insight
Duplicate task_instance rows precede duplicate loads.
Heartbeat age and leadership churn warn earlier.
Rule: query for duplicates and alert on heartbeat age.
🎯 Key Takeaway
The metadata DB exposes double-scheduling before the data does.
Stale heartbeats and churn are the earlier, quieter signals.
Punchline: alert on the tell, not on the aftermath.

6. Health Endpoints and Liveness

A process can be alive and useless. The scheduler health endpoint answers the two questions that matter: is the scheduler process up, and can it reach the metadata database? Both must return healthy, or your HA setup is a demo.

Liveness for the fleet means polling those endpoints on a schedule and alerting on the answer, not on process exit codes. A scheduler that lost its DB connection exits nothing — it hangs. The endpoint catches it; the process monitor doesn't.

The metrics that complete the picture are scheduler_heartbeat freshness and leadership stability. Prometheus, CloudWatch, or a cron curl all work — the mechanism matters less than the fact that a human gets paged before the queue does.

Rehearse the failover too: kill scheduler-1 in staging and measure how long scheduler-2 takes over. The first time you run that test should be in staging, not in the middle of the night.

The declaration of death is configurable too: scheduler_health_check_threshold sets how stale a heartbeat must be before a scheduler is declared dead and its tasks become adoptable — tune it against your DB latency so a slow database doesn't trigger phantom failovers.

health_checks.shBASH
1
2
3
4
5
6
7
8
9
10
11
# The two answers that matter
curl -s http://localhost:8793/health
# {"metadatabase": "healthy", "scheduler": "healthy"}

# Rehearsed failover, in staging:
# 1. kill scheduler-1
# 2. watch scheduler-2 claim leadership
watch -n 5 'curl -s http://scheduler-2:8793/health'

# 3. prove runs still execute (and only once)
airflow dags list-runs market_etl --state running
📊 Production Insight
Health endpoints answer alive-and-connected, not just alive.
Process monitors miss hung schedulers; endpoints catch them.
Rule: poll endpoints, alert on heartbeat freshness.
🎯 Key Takeaway
Liveness is a question the endpoint answers twice.
Heartbeat freshness is the metric that predicts failover.
Punchline: test the takeover before the outage tests you.

7. Incident Deep Dive: Failure Flow → Fix

Replay our night. We scaled from one scheduler to two for a planned HA rollout. Both schedulers parsed the same DAG folder, wrote heartbeats to the same Postgres, and — with ha_interval at default and no coordination review — both claimed the same DAG runs. The grid view filled with duplicate runs; the warehouse loaded every partition twice.

The failure flow was textbook: overlapping DAG parsing produced stale snapshots, the lease machinery accepted both claims, and nothing at the concurrency layer capped the duplicate execution. The database happily served two healthy clients asking for the same work. The first scream came from the data team at 06:00.

The fix had three layers. First, coordination: ha_interval tuned deliberately, identical env blocks on both schedulers, one shared DAG source. Second, defense in depth: max_active_tasks_per_dag capped so a duplicate claim could never fully double-execute. Third, detection: scheduler_heartbeat metrics wired to alerting, plus the duplicate-claim query in the runbook.

HA never double-runs a healthy setup by accident. It double-runs a sloppy one — now ours is the former.

📊 Production Insight
Duplicates came from claims, not from task logic.
Concurrency caps turned claims into harmless retries.
Rule: pair every HA rollout with duplicate detection.
🎯 Key Takeaway
Two schedulers double-ran us because coordination was untuned.
The fix: tuned ha_interval, concurrency caps, heartbeat alerts.
Punchline: HA you don't monitor is HA you don't have.
airflow-singleton-ha-diagram1 Double-Run Night: Failure Flow → Fix Two schedulers, untuned coordination Two schedulers, one DAG folder both write heartbeats to Postgres Both claim the same DAG runs ha_interval at default, no review Every task double-executed warehouse loaded every row twice The three-layer fix Tuned coordination ha_interval + identical env on both Concurrency safety net max_active_tasks_per_dag capped Detection wired in heartbeat alerts and duplicate query THECODEFORGE.IO
thecodeforge.io
Airflow Singleton Ha
● Production incidentPOST-MORTEMseverity: high

The Scheduler That Double-Ran

Symptom
After rolling out a second scheduler, task instances completed in parallel pairs. The grid view showed duplicate runs of the same DAG, the warehouse ingested the same rows twice, and the first alert came from the data team, not the platform team.
Assumption
The team assumed more schedulers meant simple redundancy: one would lead, the other would idle, and Airflow would handle the handoff. Nobody had touched ha_interval or audited how schedulers coordinate.
Root cause
Two schedulers with overlapping DAG parsing and untuned ha_interval semantics double-queued task instances. Both schedulers claimed the same work from the metadata DB, and the database never knew the difference — it just saw two healthy clients asking for the same jobs.
Fix
Configured the schedulers with proper HA settings — tuned ha_interval and coordination-aware DAG processing — capped per-DAG concurrency so a duplicate claim could never double-execute, and added scheduler_heartbeat metrics to alert on liveness and leadership churn. The database floor was verified against the docs too: PostgreSQL 12+ with use_row_level_locking=True.
Key lesson
  • HA schedulers are not standby replicas; every one of them actively claims work.
  • Coordination through the metadata DB is only as safe as the knobs that govern it.
  • Concurrency caps are the safety net: even if two schedulers claim a task, only one slot should exist.
  • Monitor scheduler_heartbeat and leadership churn before you trust any HA rollout.
Production debug guideSymptom to Action5 entries
Symptom · 01
Task instances completing twice, in parallel pairs
Fix
Check task_instance rows in the metadata DB for duplicate dag_run/task pairs, then audit scheduler counts, ha_interval, and per-DAG concurrency caps.
Symptom · 02
Scheduler heartbeat stale but no crash
Fix
Check the scheduler health endpoint and log stream; a scheduler that can't reach the DB looks alive in the process list and dead to the fleet.
Symptom · 03
Failover took minutes instead of seconds
Fix
Review ha_interval and the scheduler's election behavior; long failover is a tuning problem, not a restart problem.
Symptom · 04
Schedulers disagree on which DAGs exist
Fix
Check the DAG processor pool and parsing configuration on each scheduler — overlapping or competing parsers create stale, duplicate DAG snapshots.
Symptom · 05
DB latency climbing with two schedulers
Fix
Check connection pool settings and query volume; two schedulers roughly double metadata DB traffic, and the DB becomes the HA ceiling.
★ Quick Debug Cheat SheetCommon Airflow HA issues and immediate actions.
DAGs double-running after adding a second scheduler
Immediate action
Check for duplicate task instances in the metadata DB
Commands
SELECT dag_id, run_id, task_id, COUNT(*) FROM task_instance GROUP BY 1,2,3 HAVING COUNT(*) > 1;
airflow config get-value scheduler ha_interval
Fix now
Tune ha_interval and cap max_active_tasks_per_dag; restart schedulers one at a time.
Scheduler heartbeat stale+
Immediate action
Hit the scheduler health endpoint
Commands
curl -s http://localhost:8793/health
docker compose logs scheduler --since 30m | grep -i heartbeat
Fix now
Restart the scheduler and wire heartbeat alerts to your pager.
Failover takes minutes after a scheduler dies+
Immediate action
Check election and heartbeat settings
Commands
airflow config get-value scheduler scheduler_heartbeat_sec
airflow config get-value scheduler ha_interval
Fix now
Lower ha_interval for faster takeover; keep heartbeats below it.
Schedulers see different DAG sets+
Immediate action
Audit the DAG processor pool
Commands
airflow config get-value scheduler parsing_processes
airflow config get-value scheduler min_file_process_interval
Fix now
Share one DAG source and let the processor pool coordinate; verify each scheduler parses the same folder.
Single Scheduler vs HA Scheduler Fleet
AspectSingle SchedulerTwo Schedulers
Failure protectionScheduler death stops all schedulingOne dies, the other keeps claiming work
CoordinationNone neededLeases, heartbeats, ha_interval tuning
Metadata DB loadBaselineRoughly doubled — Postgres required
DAG parsingOne processor poolTwo pools — shared source, identical config
Duplicate riskNone from coordinationReal unless concurrency caps and tuning are set
MonitoringOne endpointPer-scheduler endpoints and heartbeat freshness
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
scheduler_env.shAIRFLOW__SCHEDULER__HA_INTERVAL=302. Multiple Schedulers
db_ha_checks.sqlSELECT dag_id, run_id, task_id, COUNT(*) AS claims4. Database HA
detect_double_schedule.shairflow tasks states market_etl load_orders 2026-07-155. Detecting Double-Scheduling
health_checks.shcurl -s http://localhost:8793/health6. Health Endpoints and Liveness

Key takeaways

1
Airflow HA is multiple active schedulers coordinating through one metadata database
the DB is the brain.
2
Multi-scheduler safety requires Postgres, a supported Airflow version, and deliberately tuned coordination.
3
DAG processor pools must share one source with identical parse config, or stale snapshots breed duplicates.
4
Schedulers must read from the database primary; read replicas with lag re-create double-scheduling.
5
Alert on heartbeat freshness and leadership churn, and rehearse failover in staging before prod.

Common mistakes to avoid

4 patterns
×

Adding a second scheduler without touching coordination settings

Symptom
DAGs double-run; duplicate task instances appear in the metadata DB.
Fix
Tune ha_interval, use identical env blocks on all schedulers, and cap max_active_tasks_per_dag.
×

Running HA against SQLite

Symptom
Lock errors, scheduler crashes, and coordination failures under load.
Fix
Move the metadata DB to Postgres before running more than one scheduler.
×

Pointing schedulers at read replicas for HA

Symptom
Replication lag lets two schedulers claim the same DAG run.
Fix
Schedulers read from the database primary; reserve replicas for reporting queries.
×

Monitoring scheduler process uptime instead of heartbeat freshness

Symptom
A hung scheduler goes unnoticed for hours; failover never fires.
Fix
Poll the health endpoint and alert on scheduler_heartbeat age and leadership churn.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
How does Airflow prevent two schedulers from scheduling the same task?
Q02SENIOR
You add a second scheduler and every DAG starts running twice. How do yo...
Q03SENIOR
Design a production HA setup for Airflow. What are the constraints and t...
Q01 of 03JUNIOR

How does Airflow prevent two schedulers from scheduling the same task?

ANSWER
Schedulers coordinate through the metadata database using heartbeats, leases, and the DAG processor pool. Coordination knobs like ha_interval govern claim timing, and concurrency caps like max_active_tasks_per_dag provide a second layer of defense so duplicate claims can't double-execute.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
How many schedulers should I run for high availability?
02
What is ha_interval and what does it do?
03
Can I use SQLite as the metadata database with multiple schedulers?
04
How do I detect double-scheduled tasks early?
05
What should the scheduler health endpoint check?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

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

That's Airflow. Mark it forged?

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

Previous
Airflow Environment Configuration
26 / 37 · Airflow
Next
Airflow Testing