Home DevOps Airflow Monitoring: The Dead Scheduler Nobody Saw!
Advanced 3 min · August 1, 2026
Airflow Monitoring and Logging

Airflow Monitoring: The Dead Scheduler Nobody Saw!

Airflow monitoring means heartbeat, queue-depth, and SLA alerts.

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
August 01, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 15-20 min read
  • Airflow running in production with a Celery or Kubernetes executor.
  • Familiarity with DAGs, runs, and the scheduler component.
  • Basic knowledge of Prometheus/Grafana or CloudWatch.
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Airflow monitoring tracks five signals: scheduler heartbeat, parse lag, queue depth, task age, and SLA misses.
  • The heartbeat is a metric, not a log line — its age tells you the scheduler is alive.
  • Ship task logs to S3 or GCS so they survive worker restarts and pod eviction; remote logging costs roughly 40ms per task.
  • SLA misses turn silent pipeline failures into first-class alerts before users notice.
  • A dead scheduler ran unnoticed for 9 days in our incident; heartbeat alerts end that class of outage.
  • Alert thresholds beat dashboards: pages fire at 2 missed heartbeats, not 9 days of charts.
✦ Definition~90s read
What is Airflow Monitoring and Logging?

Airflow monitoring is the practice of tracking scheduler liveness, parse lag, queue depth, task age, and SLA misses with alert thresholds, plus shipping task logs to object storage so every run stays debuggable.

Imagine a night watchman whose job is to make sure the bakery ovens run every night.
Plain-English First

Imagine a night watchman whose job is to make sure the bakery ovens run every night. Nobody checks on the watchman himself, so when he falls asleep for nine nights, the bakery produces nothing and no one notices. Airflow monitoring is a cheap alarm on the watchman: a beeper if he misses a round, a counter of how many loaves are overdue, and a recorder that saves the oven logs in a fireproof vault instead of the trash can.

The scheduler is the heart of Airflow. It parses DAGs, creates runs, and hands tasks to the executor — and when it dies, the whole platform goes quiet without a single error. We found that out the hard way. Our scheduler process crashed, and for nine days the metadata DB kept answering health checks while the DAGs sat dark.

Monitoring Airflow isn't about pretty dashboards. It's about five metrics that tell you the platform is alive, processing, and on time: scheduler heartbeat, parse lag, queue depth, task age, and SLA misses. Two of those — the heartbeat and the SLA miss — are the ones everyone skips.

Logs are the other half. Task logs written to the local disk vanish when a worker dies or a pod is evicted; shipping them to S3 or GCS means you can still debug the run that failed at 3 AM.

A dead scheduler is a binary event. Your monitoring should treat it like one.

1. The Five Signals That Matter

Monitoring Airflow is not a dashboard contest. Five signals tell you the platform is alive, on time, and producing the logs you'll need when it isn't.

The heartbeat measures scheduler liveness. Parse lag tells you how far behind the scheduler is. Queue depth shows work waiting for an executor. Task age exposes tasks that should have finished. SLA misses catch data that's late even when DAGs run green.

Everything else — CPU, memory, disk — is infrastructure noise until one of these five breaks.

Five signals, five alerts. That's the whole practice.

Airflow emits these over StatsD — statsd_on = True, statsd_host, statsd_port = 8125, statsd_prefix — or, in Airflow 3, OpenTelemetry. A statsd_exporter bridges the UDP stream to Prometheus scrape endpoints, and its mapping config is what turns Airflow's dag- and task-embedded metric names into queryable labels.

Schedule delay — the gap between a DAG's scheduled start and its actual start — is the leading indicator in the set: it starts rising before the executor saturates, while everything else still looks healthy.

signals.promqlPROMQL
1
2
3
4
5
6
# The five signals as Prometheus queries
scheduler_heartbeat: max(time() - airflow_scheduler_heartbeat_last_finished{})
parse_lag: max(time() - airflow_dag_processing_last_run_seconds_ago{})
queue_depth: sum(airflow_executor_queued_tasks{})
task_age: max(time() - airflow_task_instance_created{})
sla_misses: sum(airflow_sla_missed_dags{})
Output
scheduler_heartbeat: 2.4
parse_lag: 18.7
queue_depth: 41
task_age: 1380
sla_misses: 3
📊 Production Insight
Five signals, five alert rules, zero dashboards-only.
Every metric without a threshold is a museum piece.
Rule: page on the metric, not on the chart.
🎯 Key Takeaway
Heartbeat, parse lag, queue depth, task age, SLA.
That's the entire monitoring surface that matters.
Rule: every signal gets an alert with a real recipient.
The Five Signals That Matter The Five Signals That Matter Alert on the signal, not on the chart Scheduler heartbeat is the scheduler alive, right now Parse lag how far behind parsing has fallen Queue depth work waiting for an executor Task age tasks that should have finished SLA misses data late even when runs are green Five signals, five alerts everything else is infrastructure noise THECODEFORGE.IO
thecodeforge.io
Airflow Monitoring Logging

2. Scheduler Heartbeat: The Life Signal

The scheduler reports its heartbeat on every loop it completes. The metric stores the timestamp of the last finished cycle. The moment the process dies — or stalls — that timestamp stops moving.

Our heartbeat was never scraped. The webserver kept answering, the DB kept responding, and the scheduler metric aged like a corpse nobody checked. Heartbeat staleness is the single highest-value alert in Airflow.

Set the alert at three missed beats. With a default heartbeat every 5 seconds, that's roughly 15 seconds to a page.

Three beats. Fifteen seconds. One page.

The triggerer has its own heartbeat metric, triggerer_heartbeat — while it's down, deferred tasks accumulate — so give it the same staleness alert. And Airflow's scheduler_heartbeat counter is a rate metric: alert when its rate of change drops to zero, the dead-process signature.

Mental Model
Heartbeat as Liveness, Not Logs
Logs prove a process existed. A heartbeat proves it still exists, right now.
  • Errors only exist if the process survives to write them.
  • A killed process leaves zero error logs — only a silent gap.
  • The heartbeat timestamp turns that gap into a measurable, alertable number.
📊 Production Insight
A dead process writes no error logs.
Only the missing heartbeat reveals it.
Rule: alert on heartbeat age, not scheduler log lines.
🎯 Key Takeaway
The heartbeat is a timestamp, not a status.
Stale heartbeat equals dead scheduler, no exceptions.
Rule: page on three missed beats — about 15 seconds.
The Scheduler Heartbeat: Liveness, Not Logs The Scheduler Heartbeat: Liveness, Not Logs A timestamp that turns silent death into a 15s page Every loop completes heartbeat timestamp is stored Timestamp keeps moving scheduler is alive Timestamp stops moving dead or stalled — silently A killed process writes no logs only a silent gap remains Alert: 3 missed beats ≈ 15 seconds page on the metric, not the chart THECODEFORGE.IO
thecodeforge.io
Airflow Monitoring Logging

3. Shipping Task Logs to Object Storage

Task logs land on the worker's local disk by default. Local disk is ephemeral: workers get recycled, pods get evicted, containers get recreated — and the logs go with them.

Remote logging writes every task log to S3, GCS, or Azure Blob at the end of each task. The overhead is small — roughly 40ms per task on a typical run — and the payoff is huge: you can debug any failed run from any month, on any machine.

Remote logging is the difference between a post-mortem and a shrug.

airflow.cfgINI
1
2
3
4
[logging]
remote_logging = True
remote_base_log_folder = s3://airflow-logs-prod/logs
remote_task_handler = airflow.providers.amazon.aws.log.s3_task_handler.S3TaskHandler
Output
Task logs now stream to s3://airflow-logs-prod/logs after each run.
📊 Production Insight
Local logs are a refund guarantee you'll never collect.
Remote logging costs ~40ms per task.
Rule: enable remote logging before the first incident.
🎯 Key Takeaway
Task logs die with their worker.
Object storage keeps them alive for forensics.
Rule: set the remote base folder today, not after the outage.

4. SLA Misses as First-Class Alerts

SLA means the latest acceptable finish time for a DAG run. Airflow records a miss in the sla_miss table when a run crosses it — but recording is all it does unless you wire the callback.

Set sla in default_args, point sla_miss_callback at a function that pages the on-call, and a late DAG becomes a human problem in minutes instead of a user problem in days.

Our dead scheduler produced nine days of SLA misses. The table recorded every one. Nobody was looking at the table.

sla_miss.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from datetime import timedelta
from airflow.models.dag import DAG


def on_sla_miss(context):
    dag_id = context["dag"].dag_id
    notify_pagerduty(
        incident_title=f"SLA miss: {dag_id}",
        description=(
            f"{dag_id} missed its SLA for run "
            f"{context['execution_date']}"
        ),
    )


default_args = {
    "owner": "data-eng",
    "sla": timedelta(hours=3),
    "sla_miss_callback": on_sla_miss,
}
Output
PagerDuty incident created: SLA miss: orders_daily
📊 Production Insight
SLA misses are recorded, not reported.
The callback is what turns them into pages.
Rule: wire sla_miss_callback to your pager, or the table is decoration.
🎯 Key Takeaway
Green DAGs can still miss SLA.
The sla_miss table never lies.
Rule: every prod DAG gets an SLA and a callback.

5. Notification Integrations: PagerDuty, Slack, Email

Alerts are only as good as their routing. Email buries you, Slack channels scroll, PagerDuty wakes a human. A sane stack uses all three with different severities.

Pages for heartbeat staleness, queue-depth overflow, and SLA misses. Slack for warnings and DAG-level failures. Email for the digest that nobody reads but compliance likes.

The rule of thumb: if an alert doesn't page, it's a log entry, not an alert.

📊 Production Insight
One channel per severity, not one channel for all.
If it can't page a human, it's not an alert.
Rule: page on liveness and SLA; chat on failures.
🎯 Key Takeaway
Routing decides whether an alert acts.
Pages wake people; channels scroll away.
Rule: heartbeat and SLA alerts go to the pager, period.

6. The 9-Day Silent Failure: Alert Anatomy

Tuesday, 02:14 — the scheduler container ran out of memory and died. Nothing else changed. The webserver kept serving, the UI kept rendering, and the metadata DB kept answering health probes.

Day 1: the first queued task ages past an hour. No alert — task age wasn't monitored.

Day 3: the first SLA miss lands in sla_miss. No callback — it was never wired.

Day 9: a user asks why yesterday's dashboard is stale. That's when the outage ends — not with a page, but with a ticket.

The alert that should have existed is a single rule: heartbeat age over 60 seconds. It would have fired at 02:15, one minute after the process died.

⚠ Dashboards Don't Page
A dashboard is a museum of yesterday's problems. If your monitoring has charts but no alert rules, you are not monitoring — you are decorating.
📊 Production Insight
One rule would have caught this at minute one.
Nine days of silence is a process failure.
Rule: heartbeat staleness over 60 seconds pages immediately.
🎯 Key Takeaway
Silent failures destroy trust faster than loud ones.
The first SLA miss is the earliest sane alarm.
Rule: every metric needs an owner and a pager.
The 9-Day Silent Failure Timeline The 9-Day Silent Failure Timeline A dead scheduler that never paged anyone Tue 02:14 — scheduler OOM-killed webserver, UI, DB stay green Day 1 — queued task ages past 1h task age was never monitored Day 3 — first SLA miss recorded sla_miss_callback was never wired Day 9 — user reports stale dashboard outage ends with a ticket, not a page One rule would have caught it heartbeat over 60s, firing at 02:15 THECODEFORGE.IO
thecodeforge.io
Airflow Monitoring Logging

7. Runbooks That Survive

An alert without a runbook is a panic with extra steps. When the page fires at 3 AM, the person on call needs a checklist: confirm the signal, check the obvious causes, restart safely, verify recovery.

Write the runbook when you write the alert. Two paragraphs, real commands, exact expectations. Our dead-scheduler runbook now says: check heartbeat age, pull scheduler logs, restart the container, verify the next heartbeat lands within 60 seconds.

A runbook that survives is one that's been executed in a drill. Run the drill on a Tuesday afternoon, not during the incident.

📊 Production Insight
Alert and runbook ship together, or neither exists.
Drills make runbooks survive contact with 3 AM.
Rule: rehearse every page path twice a year.
🎯 Key Takeaway
An alert without a runbook is a panic.
A runbook without a drill is fiction.
Rule: drill the heartbeat page twice a year.
● Production incidentPOST-MORTEMseverity: high

The Scheduler That Died on a Tuesday

Symptom
No DAG runs completed for nine days. The webserver stayed up, the UI rendered fine, and the metadata DB answered every health check, but every scheduled run sat in queued forever.
Assumption
The team assumed the platform was healthy because the webserver and database were up and the UI showed no errors.
Root cause
The scheduler container was OOM-killed. No heartbeat alert existed, so nothing fired; with the scheduler down, no runs were created and no queued task was dispatched for nine days.
Fix
Added heartbeat-staleness, queued-task-age, and SLA-miss alerts, enabled remote logging to S3, and added a liveness probe that restarts the scheduler container.
Key lesson
  • A healthy webserver and database do not mean a healthy scheduler.
  • The scheduler heartbeat is the single most important liveness metric in Airflow.
  • Alert on staleness, not on log lines — a killed process writes no errors at all.
  • Queue depth and task age reveal a dead platform even when the UI looks fine.
  • SLA misses convert late data into a pager event before users report it.
Production debug guideSymptom to Action5 entries
Symptom · 01
No DAG runs for hours while the UI looks healthy
Fix
Check the scheduler health endpoint and heartbeat metric age before anything else; the scheduler is the prime suspect.
Symptom · 02
Tasks stuck in queued with open worker slots
Fix
Check executor queue depth and worker health; a dead worker with a full broker queue mimics a stalled platform.
Symptom · 03
Task logs missing after a restart or eviction
Fix
Confirm remote logging is enabled; local logs die with the worker that wrote them.
Symptom · 04
Data is late but DAGs are green
Fix
Query the sla_miss table and check schedule alignment; SLA misses record what green runs hide.
Symptom · 05
Heartbeat metric aging while the scheduler process runs
Fix
Look for scheduler stalls in logs and DB saturation; an alive process can still be effectively dead.
★ Quick Debug Cheat SheetReal commands for the five signals that keep an Airflow platform honest.
No DAG runs; UI looks fine
Immediate action
Check the scheduler health endpoint
Commands
curl -s http://localhost:8080/health
docker compose logs scheduler --tail=200
Fix now
Restart the scheduler and verify the heartbeat metric stops aging.
Tasks stuck in queued+
Immediate action
List DAGs and check workers
Commands
airflow dags list
airflow celery flower
Fix now
Restart dead workers; drain the broker queue and re-queue tasks.
Task logs vanish after restarts+
Immediate action
Check remote logging config
Commands
airflow config get-value logging remote_logging
airflow config get-value logging remote_base_log_folder
Fix now
Enable remote logging to S3 or GCS so logs survive workers.
SLA missed silently+
Immediate action
Inspect the sla_miss table
Commands
sqlite3 $AIRFLOW_HOME/airflow.db 'select dag_id, execution_date from sla_miss order by execution_date desc limit 10;'
airflow dags list --output table
Fix now
Wire sla_miss_callback to a pager and treat every miss as an incident.
Scheduler heartbeat aging+
Immediate action
Query the heartbeat metric
Commands
max(time() - airflow_scheduler_heartbeat_last_finished)
kubectl logs -l component=scheduler --tail=200
Fix now
Restart the scheduler; then fix the stall that aged the heartbeat.
The Five Monitoring Signals
SignalDetectsHealthy thresholdCost of ignoring
Scheduler heartbeatDead or stalled schedulerAge under 60 secondsDays of silent downtime
Parse lagScheduler falling behind on DAG parsingUnder 60 secondsRuns fire late, then never
Queue depthExecutor or worker failureNear zero outside peakTasks stuck queued forever
Task ageTasks that should have finishedUnder one schedule intervalSlow tasks eat slots
SLA missData late despite green runsZeroUsers report before you do
Remote log availabilityLost forensic evidenceEvery task writes to S3Post-mortems become shrugs
⚙ Quick Reference
3 commands from this guide
FileCommand / CodePurpose
signals.promqlscheduler_heartbeat: max(time() - airflow_scheduler_heartbeat_last_finished{})1. The Five Signals That Matter
airflow.cfg[logging]3. Shipping Task Logs to Object Storage
sla_miss.pyfrom datetime import timedelta4. SLA Misses as First-Class Alerts

Key takeaways

1
The scheduler heartbeat is the most important liveness metric in Airflow; alert on its age, not on log lines.
2
Five signals matter
heartbeat, parse lag, queue depth, task age, and SLA misses — everything else is noise.
3
Remote logging to S3 or GCS costs ~40ms per task and keeps every post-mortem possible.
4
SLA misses are recorded, not reported; wire sla_miss_callback to a pager or the table is decoration.
5
Every alert ships with a runbook and a drill; dashboards don't page anyone.

Common mistakes to avoid

4 patterns
×

Dashboard-only monitoring with no alert rules

Symptom
The dead scheduler is discovered by a user, not by a page
Fix
Alert on heartbeat staleness, queued-task age, and SLA misses — dashboards don't wake anyone up.
×

Task logs stay on the worker's local disk

Symptom
Logs vanish after a worker restart or pod eviction
Fix
Enable remote logging to S3 or GCS in airflow.cfg so logs survive the worker.
×

Monitoring the webserver instead of the scheduler

Symptom
The UI is up while every DAG is dead
Fix
Health-check the scheduler process and its heartbeat metric, not just port 8080.
×

SLA misses recorded but never acted on

Symptom
sla_miss rows pile up in the metadata DB with no callback
Fix
Wire sla_miss_callback to a pager and treat every miss as an incident.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does a scheduler heartbeat tell you?
Q02SENIOR
A DAG run shows success but the data is late. What do you check?
Q03SENIOR
Design monitoring for a 1,000-DAG Airflow platform. What alerts and runb...
Q01 of 03JUNIOR

What does a scheduler heartbeat tell you?

ANSWER
It records the timestamp of the scheduler's last completed loop. If that timestamp is old — say, over 60 seconds — the scheduler is either dead or stalled. It's the most reliable liveness signal in Airflow, because a killed process writes no error logs.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
How do I know if my Airflow scheduler is alive?
02
Statsd + Prometheus or CloudWatch?
03
How long should I keep task logs?
04
What is an SLA miss in Airflow?
05
Can't I just watch the Airflow UI?
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
August 01, 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 CI/CD Deployment
29 / 37 · Airflow
Next
Airflow Performance Tuning