Home DevOps Airflow Monitoring: Dead Scheduler Nobody Saw for 9 Days
Advanced 4 min · September 04, 2026
Airflow Monitoring and Logging

Airflow Monitoring: Dead Scheduler Nobody Saw for 9 Days

Airflow monitoring catches dead schedulers before 9-day silent outages strike.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
Production
production tested
September 04, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 30 min
  • A running Airflow 3.x instance you can query
  • Basic familiarity with DAG runs and task states
  • Access to a metrics store like Prometheus or CloudWatch
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Airflow monitoring means watching scheduler heartbeats, queued-task age, and SLA misses so silent stalls page you within minutes
  • Key components: scheduler heartbeat job rows, Prometheus or CloudWatch metrics, remote task logs on S3 or GCS, and sla_miss_callback notifications
  • Performance insight: a stale heartbeat older than 5 minutes plus p95 queued-task age over 10 minutes catches nearly every scheduling stall before users do
  • Production insight: one team lost 9 days of DAG runs because process supervision looked green while the scheduler was dead; heartbeat alerts would have paged in 6 minutes
  • Biggest mistake: keeping task logs only on local worker disks, where a pod eviction destroys the only evidence of a failure
✦ Definition~90s read
What is Airflow Monitoring and Logging?

Airflow monitoring is the practice of alerting on scheduler heartbeats, queued-task age, and SLA misses so silent stalls page you in minutes. It pairs metrics pipelines like Prometheus with remote task logs on S3 or GCS.

Think of Airflow like an airport control tower directing planes.
Plain-English First

Think of Airflow like an airport control tower directing planes. Monitoring is the radar screen showing whether the controllers are awake, how long planes circle before landing, and whether any flight missed its promised arrival time. Without that radar, the tower can go dark and nobody notices until passengers complain.

Nobody checks the scheduler on a quiet Tuesday. DAGs go green, the Grid view looks calm, and the team ships features. That's exactly when a dead scheduler hides best.

One team learned this the expensive way. Their scheduler died during a routine node rotation, every DAG silently stopped, and nine days passed before finance asked where the revenue report went. No page. No Slack ping. Nothing.

This guide shows you the three heartbeat metrics that catch that failure in minutes, how to ship logs to S3 or GCS so evidence survives pod evictions, and how SLA misses become first-class alerts. Nine days. Zero runs.

The Three Metrics That Prove Airflow Is Alive

Three numbers tell you Airflow is alive. Scheduler heartbeat age shows the scheduler wrote to the job table recently. Queued-task age shows tasks aren't starving in pools. SLA miss count shows business promises still hold.

Heartbeat age is the king metric. The scheduler emits it every few seconds, and anything over 5 minutes stale means scheduling stopped, whether the process died or wedged on a DB lock. You'll graph it as time minus last heartbeat timestamp.

Queue age catches what heartbeats miss. A live scheduler with a wedged pool still schedules nothing useful, and p95 queued-task age per pool exposes exactly that. Track depth too, but page on age.

Pick your metrics pipe on purpose. StatsD is the long-standing path: set statsd_on = True with host, port 8125, and a prefix, then bridge UDP to Prometheus with statsd_exporter and mapping rules that turn DAG- and task-embedded names into labels. OpenTelemetry is the forward-looking path in Airflow 3: flip otel_on in [metrics], point at your OTel Collector, and let it route to Prometheus. If you're starting fresh, OTel's ecosystem wins long-term. One gotcha either way: StatsD drops tag mappings by default, so enable a tagged wire format (statsd_influxdb_enabled or statsd_datadog_enabled) or your per-DAG labels never materialize in Prometheus.

Don't skip the /health probe. The API server's /health endpoint reports the metadata DB plus fresh heartbeats for scheduler, triggerer, and DAG processor, and it's what your liveness checks should hit. Tie it to metrics: fire an Alertmanager rule when scheduler-heartbeat lag crosses 60s so you don't hand-refresh a health page at 3 AM. Five signals stay core: heartbeat, queue backlog, failure rate, parse time, and pool utilization — a pool pinned at 100% with waiters piling up means its slots need a recount, not a bigger dashboard.

prometheus/airflow-alerts.ymlYAML
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
26
27
28
groups:
- name: airflow-scheduler
  interval: 30s
  rules:
  - alert: AirflowSchedulerDead
    expr: time() - airflow_scheduler_heartbeat_timestamp > 300
    for: 2m
    labels:
      severity: page
      team: data-platform
    annotations:
      summary: Scheduler heartbeat stale over 5 minutes
      runbook: https://wiki.acme.io/runbooks/airflow-scheduler-dead
  - alert: AirflowQueueStarving
    expr: histogram_quantile(0.95, airflow_queued_task_age_seconds) > 600
    for: 5m
    labels:
      severity: page
      team: data-platform
    annotations:
      summary: P95 queued-task age above 10 minutes
  - alert: AirflowSLAMiss
    expr: increase(airflow_sla_missed_total[15m]) > 0
    labels:
      severity: ticket
      team: dag-owner
    annotations:
      summary: SLA miss on a tier-1 DAG in the last 15 minutes
⚠ Heartbeat First, Process Second
A running process is not a scheduling scheduler. Only the heartbeat row proves work is happening. Check the heartbeat first, the process second.
📊 Production Insight
Heartbeat age over 5 minutes means scheduling stopped, dead or wedged. Queue age per pool exposes starvation that flat depth hides. Rule: page on age, graph depth for context.
🎯 Key Takeaway
Heartbeat age, queued-task age, and SLA misses cover dead schedulers, starved pools, and late outcomes. Page on the first two, ticket on the third unless it repeats.

Shipping Task Logs to S3 and GCS

Local task logs die with the worker. On Kubernetes a pod eviction wipes the only copy of the traceback you need, and even on VMs a disk-full event takes logs with it. Remote logging fixes this by writing every task log to object storage as it streams.

Setup takes four settings. Point REMOTE_BASE_LOG_FOLDER at your bucket, give workers a conn id with write-only IAM permissions, and turn on DELETE_LOCAL_LOGS with a 7-day interval so disks don't fill. The UI reads remote logs transparently, so developers notice nothing except logs that never vanish.

Verify before you trust it. Run one task, list the bucket prefix, and confirm the log object exists. Then kill a worker mid-task on staging and confirm the log survives. You'll thank yourself during the next real eviction.

config/airflow-logging.envBASH
1
2
3
4
5
6
7
8
9
10
11
# airflow.cfg logging section for production log shipping
AIRFLOW__LOGGING__REMOTE_LOGGING=True
AIRFLOW__LOGGING__REMOTE_BASE_LOG_FOLDER=s3://acme-airflow-logs/prod
AIRFLOW__LOGGING__REMOTE_LOG_CONN_ID=aws_logs_writer
AIRFLOW__LOGGING__DELETE_LOCAL_LOGS=True
AIRFLOW__LOGGING__DELETE_LOCAL_LOGS_INTERVAL=7

# verify from any host
firstname=$(airflow config get-value logging remote_base_log_folder)
echo "remote logs go to: $firstname"
aws s3 ls s3://acme-airflow-logs/prod/ --human-readable | head -20
📊 Production Insight
Local logs vanish on pod eviction, remote logs don't. Keep 7 days local for fast reads, everything in S3. Rule: verify with one real task run, not just config.
🎯 Key Takeaway
Remote logging to object storage makes task evidence eviction-proof. Four settings, one bucket listing to verify, and logs survive every worker death.

SLA Misses as First-Class Alerts

SLA misses are the only alert the business understands. Tasks can retry their way to success 4 hours late while every technical dashboard stays green. The sla parameter draws the line: this DAG must finish within 2 hours of its run, or someone hears about it.

Wire sla_miss_callback to the team that owns the outcome, not a general channel. Finance DAGs page finance-adjacent engineers in #data-incidents; experimental DAGs file tickets. Routing matters because an SLA alert the wrong team ignores is the same as no alert.

Start strict on tier-1 DAGs only. Five SLA alerts on critical pipelines beat fifty noisy ones everyone mutes. You'll tune thresholds with a month of data, and each adjustment gets a note in the runbook.

Heads-up for Airflow 3: classic SLA monitoring is gone. The old sla_miss table and sla_miss_callback behavior was removed because its logical_date math confused everyone. The replacement is the Deadline concept — you state the intent explicitly (reference time plus grace period, then run this callback) instead of inheriting N-minutes-from-logical-date. If you're migrating a 2.x DAG, don't port sla= lines one-for-one; redesign each alert on Deadline and confirm the exact API shape in the airflow.apache.org docs for your 3.x minor, since this area is still settling version to version. Keep the same rule: a lateness record nobody pages on is decoration.

dags/revenue_daily.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
26
import datetime
import pendulum
from airflow.sdk import dag
from airflow.providers.slack.notifications.slack import send_slack_notification

BUSINESS_SLA = datetime.timedelta(hours=2)

sla_alert = send_slack_notification(
    slack_conn_id="slack_data_alerts",
    text="SLA miss on {{ dag.dag_id }}: run {{ run_id }} is late.",
    channel="#data-incidents",
)

@dag(
    dag_id="revenue_daily",
    schedule="0 6 * * *",
    start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
    catchup=False,
    sla=BUSINESS_SLA,
    sla_miss_callback=sla_alert,
    tags=["tier-1", "finance"],
)
def revenue_daily():
    pass

revenue_daily()
📊 Production Insight
A DAG can succeed 4 hours late with zero failed tasks. SLA catches lateness that retries hide. Rule: every tier-1 DAG gets an sla plus a callback.
🎯 Key Takeaway
SLA measures lateness, retries measure failure, and only SLA tells you the business impact. Put sla on tier-1 DAGs and route misses to the owning team.

PagerDuty, Slack, and Email That Actually Fire

One channel per severity keeps alerts actionable. Pages go to PagerDuty for heartbeat and queue-age failures. Slack gets SLA misses and failure-rate warnings where the owning team already lives. Email digests cover daily summaries nobody needs at 3 AM.

Keep the signal path short. Airflow notifiers post straight to Slack webhooks, and a small PagerDuty integration turns repeated SLA misses into incidents. You'll avoid the classic trap of alerts landing in a channel with 200 members and zero owners.

Test the path monthly. A rotated Slack webhook silently breaks notifications, which recreates the 9-day silence with better intentions. Send a test alert from staging after every credential rotation.

📊 Production Insight
Alerts in ownerless channels get ignored like no alerts at all. Route by severity to the smallest owning group. Rule: test webhooks after every rotation.
🎯 Key Takeaway
Route pages to PagerDuty, misses to the owning team's Slack, digests to email. Test every webhook monthly or rotations will silently break them.

Anatomy of the 9-Day Silent Failure

The 9-day outage had a boring anatomy. Node rotation killed the scheduler, the job-table heartbeat froze, and run creation stopped. Depth metrics stayed flat because flat looks healthy when nothing arrives. The webserver masked everything by serving a perfect UI over stale data.

Three alerts would have caught it in minutes. Heartbeat age pages at 5 minutes stale. Queue age pages when the first tasks starve. An SLA miss on the revenue DAG fires when the business promise breaks. Total cost: two Prometheus rules and one callback.

The lesson isn't more dashboards. It's alerting on absence: no heartbeat, no runs, no logs shipped. You'll catch the next silent failure by watching for what should exist but doesn't.

Mental Model
Silence Is Failure
Silent schedulers don't crash loudly. They stop writing heartbeats, stop creating runs, and let every dashboard stay green. Your monitoring must assume silence is failure, not calm.
📊 Production Insight
Flat metrics looked healthy because nothing arrived to move them. Absence of runs is the signal, not the noise. Rule: alert on what should exist but doesn't.
🎯 Key Takeaway
The outage hid because every dashboard measured presence, not absence. Alert on missing heartbeats, missing runs, and missing logs, and silence becomes loud.

Runbooks That Survive On-Call Rotations

A runbook turns a 3 AM page into a checklist. One page per alert: symptom, first three commands, escalation path, and the rollback step. The scheduler-dead runbook starts with airflow jobs check and ends with the restart command plus verification.

Keep runbooks next to the alert. Link the wiki page in the Prometheus annotation so the paged engineer opens it in one click. You'll cut mean-time-to-recovery from an hour of guessing to ten minutes of following steps.

Review runbooks after every incident. If a step didn't help, delete it. If a command was missing, add it. A runbook that survives three incidents unchanged is either perfect or unread, and it's never perfect.

📊 Production Insight
Pages without runbooks become guessing sessions at 3 AM. Three commands plus escalation fits one page. Rule: review the runbook after every single incident.
🎯 Key Takeaway
One page per alert with commands, escalation, and rollback beats a wiki nobody opens. Link it in the alert annotation and prune it after every incident.
● Production incidentPOST-MORTEMseverity: high

The Dead Scheduler Nobody Saw for 9 Days

Symptom
DAG runs stopped appearing across the whole instance while the UI loaded normally. Task counts froze, the Grid view showed nothing new, and CPU on the scheduler host dropped to idle. Because the webserver and metadata DB were healthy, every infrastructure dashboard stayed green. The first human signal came 9 days later from finance, not engineering.
Assumption
The team assumed systemd had them covered. The scheduler ran as a supervised service, restarts were automatic, and the UI stayed up because the webserver and metadata DB were healthy. Green dashboards meant a healthy platform, or so everyone believed. Nobody had defined what a dead scheduler would even look like in their monitoring.
Root cause
The scheduler process died during the rotation and was never restarted cleanly. No heartbeat alert existed, so nothing detected the stale job row in the metadata DB. Queued-task monitoring tracked depth but every DAG simply stopped producing runs, so depth stayed flat and healthy-looking. With no SLA callbacks configured, late business outcomes generated zero notifications for over a week.
Fix
The team added three alerts that same week. A Prometheus rule pages when the scheduler heartbeat metric goes stale for 5 minutes. A second rule pages when p95 queued-task age crosses 10 minutes in any pool. Every tier-1 DAG got an sla plus an sla_miss_callback posting to Slack and PagerDuty. Task logs were shipped to S3 with a 7-day local retention, and a one-page runbook was pasted into the on-call wiki. The next scheduler stall paged within 6 minutes.
Key lesson
  • Process supervision is not scheduler monitoring. Alert on the heartbeat row in the metadata DB, because a wedged-but-running scheduler passes every process check while scheduling nothing.
  • Queue depth without queue age hides starvation. Track how long tasks wait, per pool, and page on age so one wedged pool cannot hide behind a healthy-looking total.
  • Every silent failure needs a runbook before the next one. Heartbeat, queue age, and SLA alerts are only useful if the on-call engineer knows the first three commands to run.
Production debug guideFour failure patterns behind most silent Airflow outages, with the exact commands that expose each one.4 entries
Symptom · 01
No new DAG runs appear for over 30 minutes but the UI loads fine
Fix
Run airflow jobs check --job-type SchedulerJob. If it reports an unhealthy job, check ps aux | grep scheduler on the host, then tail the scheduler logs for the last traceback. Restart the scheduler and watch airflow jobs check turn green before declaring victory. Also curl the API /health endpoint — it shows scheduler, triggerer, and DAG-processor freshness plus DB status in one response.
Symptom · 02
Tasks sit in queued for over 10 minutes while workers look idle
Fix
Query the metadata DB for old queued tasks: select dag_id, task_id, now() - queued_dttm as age from task_instance where state='queued' order by age desc limit 20. If one pool dominates, run airflow pools list to check slot counts, then look for sensors holding slots in poke mode.
Symptom · 03
Task log view shows log file does not exist after a worker restart
Fix
Run airflow config get-value logging remote_logging and airflow config get-value logging remote_base_log_folder. If remote logging is off, that is your gap. Then verify bucket access with aws s3 ls s3://acme-airflow-logs/prod/ and fix the worker IAM role before rerunning the task.
Symptom · 04
Business report lands hours late with no alert fired
Fix
Check the sla_miss table: select dag_id, execution_date from sla_miss order by timestamp desc limit 10. If rows exist with no alert, your sla_miss_callback is missing or pointing at a dead webhook. Test the callback with airflow dags test on a DAG with a 1-second sla.
★ Airflow Silent-Stall Debug Cheat SheetThe five checks that expose a silent Airflow stall in under ten minutes. Run them in order before paging the platform team.
No new DAG runs in 30+ minutes, UI loads fine
Immediate action
Check the scheduler heartbeat row in the metadata DB, not the OS process
Commands
airflow jobs check --job-type SchedulerJob
ps aux | grep -i airflow-scheduler | grep -v grep
Fix now
Restart the scheduler process, then rerun airflow jobs check until it reports healthy. If it dies again within minutes, pull the scheduler log tail and look for DB connection errors before anything else.
Tasks stuck in queued while workers look idle+
Immediate action
Find which pool holds the oldest queued tasks
Commands
airflow pools list
airflow tasks states-for-dag-run --help
Fix now
Free the wedged pool: cancel or mark success on the stuck sensor tasks, convert long pokes to deferrable sensors, and raise pool slots only after the blockage is cleared.
Task logs missing after worker restart+
Immediate action
Confirm remote logging is on and the bucket is reachable
Commands
airflow config get-value logging remote_logging
aws s3 ls s3://acme-airflow-logs/prod/ --human-readable | head -20
Fix now
Enable remote logging to S3 or GCS, fix the worker IAM role, and rerun one failed task to confirm logs land in the bucket.
Tier-1 DAG failing with no clear owner paged+
Immediate action
List recent failed runs on the business-critical DAG
Commands
airflow dags list-runs -d revenue_daily --state failed | head -20
airflow tasks list revenue_daily | head -30
Fix now
Clear the failed runs with airflow tasks clear, fix the root cause from the remote logs, and backfill only the affected dates with a concurrency cap.
Metrics dashboard flat or empty for Airflow+
Immediate action
Verify metrics export is enabled on scheduler and workers
Commands
airflow config get-value metrics statsd_on
curl -s localhost:9102/metrics | grep -i scheduler | head -20
Fix now
Enable StatsD metrics with the Prometheus exporter sidecar, then import the heartbeat-age and queue-age dashboard before the next deploy.
Airflow Monitoring Signals Compared
SignalWhat it catchesAlert thresholdWakes someone up?
Scheduler heartbeat ageDead or wedged scheduler, DB stalls> 5 min since last heartbeatYes, page
Queued-task age (p95)Starved pools, stuck sensors, dead workers> 10 min in queuedYes, page
SLA miss eventsLate business outcomes, slow DAGsAny miss on tier-1 DAGsSlack, page on repeat
Task failure rateBroken code, bad deploys, flaky deps> 5% over 15 minSlack first
Triggerer backlogDeferred tasks piling past trigger capacity> 500 queued triggersSlack, scale triggerer
Log-shipping lagLost evidence, full disks, IAM driftRemote logs > 15 min staleTicket, fix by next run
Pool utilizationOne pinned pool throttling a whole platformAny pool at 100% with waiters > 5 minSlack, resize or reprioritize
DAG parse timeHeavy DAGs delaying every schedule decisionp95 parse > 30s or rising week over weekTicket, profile top-level code
⚙ Quick Reference
3 commands from this guide
FileCommand / CodePurpose
prometheusairflow-alerts.ymlgroups:The Three Metrics That Prove Airflow Is Alive
configairflow-logging.envAIRFLOW__LOGGING__REMOTE_LOGGING=TrueShipping Task Logs to S3 and GCS
dagsrevenue_daily.pyfrom airflow.sdk import dagSLA Misses as First-Class Alerts

Key takeaways

1
Alert on scheduler heartbeat age over 5 minutes, and back it with the /health probe
process checks miss wedged schedulers every time.
2
Track queued-task age per pool plus pool utilization, not just queue depth, so a single pinned pool can't hide behind healthy averages.
3
Ship task logs to S3 or GCS (about 40ms per task) so evidence survives worker eviction; tier to Glacier after 30 days and keep 90.
4
On Airflow 3, replace SLA thinking with Deadline alerts stated as reference time plus grace period
sla_miss_callback patterns don't port one-for-one.
5
Choose StatsD with tagged wire format or OTel via Collector deliberately, and write a one-page runbook per alert so any on-call can respond in minutes.

Common mistakes to avoid

4 patterns
×

Watching the scheduler process instead of its heartbeat

Symptom
systemd says the scheduler is active, yet no DAG has produced a run in hours and the UI shows stale last-parse times.
Fix
Alert on the scheduler heartbeat job row in the metadata DB, not the OS process alone. Run airflow jobs check --job-type SchedulerJob in your liveness probe and page when the latest heartbeat is older than 5 minutes. Belt and suspenders beats a silent fleet. Add the /health endpoint to the same probe, and if you use StatsD, enable a tagged wire format so per-DAG labels survive the exporter hop.
×

Keeping task logs only on the worker's local disk

Symptom
A failed task's logs vanish when Kubernetes evicts the worker pod, leaving you with a red task and zero evidence.
Fix
Ship logs to S3 or GCS with AIRFLOW__LOGGING__REMOTE_LOGGING=True and keep 7 days local for tail -f debugging. Verify with airflow config get-value logging remote_base_log_folder and one aws s3 ls against the bucket. On Airflow 3, pair this with pool utilization metrics — a pool pinned at 100% with starving tasks means the slot count, not the workers, is wrong.
×

Treating SLA misses as informational noise

Symptom
The finance DAG lands 3 hours late every month-end and nobody notices until the CFO asks why the report is stale.
Fix
Set an sla on every business-critical DAG and wire sla_miss_callback to Slack or PagerDuty. Treat the first SLA miss like a failed deploy: acknowledge it, note the cause, adjust the threshold only with data.
×

Alerting on queue length but not queued-task age

Symptom
Queue depth hovers at a normal 40 while a single pool's tasks sit queued for 2 hours because every slot is held by a stuck sensor.
Fix
Track queued-task age (now minus queued_dttm) per pool, not just queue length. Page when the p95 age crosses 10 minutes. Age tells you tasks are starving; length alone can look fine while one pool is wedged.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Why alert on scheduler heartbeat age instead of scheduler process livene...
Q02SENIOR
Queued-task depth versus queued-task age: which alert fires first when a...
Q03JUNIOR
What does the sla parameter on a DAG actually measure?
Q01 of 03SENIOR

Why alert on scheduler heartbeat age instead of scheduler process liveness?

ANSWER
The scheduler writes a heartbeat row to the job table every few seconds. If the process dies, the row goes stale. Alerting on process liveness misses wedged-but-running schedulers (DB lock, full disk, parse loop stuck). Alerting on heartbeat age catches both dead and wedged states, which is why the 9-day outage went unnoticed: the process was gone and nobody watched the heartbeat.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
How do I know the scheduler is actually alive, not just running?
02
Where should Airflow task logs live in production?
03
Is an SLA miss the same as a task failure?
04
How many alerts does a small team actually need?
05
Prometheus or CloudWatch for Airflow metrics?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
Verified
production tested
September 04, 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 CI/CD Deployment
29 / 37 · Airflow
Next
Airflow Performance Tuning