A disk fills up on a backup host. Nobody gets paged. The post-mortem shows the alert rule was correct: node_filesystem_avail_bytes below 10 percent for 15 minutes. The problem was that the node exporter on that host had died three weeks earlier after a package upgrade, and a rule that compares a missing series to a threshold returns nothing. Nothing is not a firing alert. The dashboard panel showed “No data”, and nobody looks at a dashboard for a host that never complains.
This is the most common blind spot I see in Prometheus setups. Threshold rules only fire on data that exists. When the data disappears, the rule goes quiet, and the quiet looks exactly like health.
Three ways a series goes missing
It helps to separate the cases, because each needs a different check:
- The target is unreachable. Prometheus still knows about the target from service discovery, scrapes fail, and
upgoes to 0. This is the easy case. - The target vanishes from discovery. A Consul registration expires, a Kubernetes pod is gone, a file_sd entry gets deleted by a bad config push. There is no
upseries at all any more, soup == 0never matches. - The target is up but a specific metric stops. The exporter answers, but a collector fails, a textfile collector script stops writing, or a relabel rule drops the series.
upis 1 and everything looks green.
Most teams cover case 1 and assume they are done.
Rules that cover all three
groups:
- name: meta-monitoring
rules:
# Case 1: target known but failing scrapes
- alert: TargetDown
expr: up == 0
for: 5m
labels:
severity: warning
annotations:
summary: " on is not scrapeable"
# Case 2: an expected job has no targets at all
- alert: JobMissing
expr: absent(up{job="node"})
for: 10m
labels:
severity: critical
annotations:
summary: "No targets discovered for job=node"
# Case 2b: fewer targets than yesterday (catches partial discovery loss)
- alert: TargetCountDropped
expr: |
count by (job) (up)
< 0.9 * count by (job) (up offset 1d)
for: 15m
labels:
severity: warning
# Case 3: a specific series stopped while the target stays up
- alert: FilesystemMetricMissing
expr: |
up{job="node"} == 1
unless on (instance)
node_filesystem_avail_bytes{mountpoint="/"}
for: 15m
labels:
severity: warning
# Textfile collector freshness (backup scripts, cron results)
- alert: TextfileStale
expr: time() - node_textfile_mtime_seconds > 3 * 3600
for: 10m
labels:
severity: warning
A few notes from running these:
absent()only works for a series you name explicitly. It takes no label set from the data, so you cannot write one genericabsent()rule for every host. Use it for whole jobs, and use theunlesspattern for per-instance checks.- The
unless on (instance)form is the workhorse. It returns every instance that is up but lacks the expected series, with labels intact, so routing and silences work normally. - The
offset 1dcomparison is noisy during planned decommissions. That is acceptable; a decommission is a change, and a change should be acknowledged. - Textfile collector metrics are the worst offenders. A backup script that writes
backup_last_success_timestampand then stops running leaves the last value in place forever if the file is not removed. Alert on freshness, not on the value.
Who watches Prometheus
All of the above assumes Prometheus and Alertmanager are themselves running and able to send. If Prometheus crashes, every rule above goes silent at once. The standard fix is a dead man’s switch: an alert that always fires.
- alert: Watchdog
expr: vector(1)
labels:
severity: none
annotations:
summary: "Alerting pipeline heartbeat"
Route it in Alertmanager to an external receiver that expects a ping every few minutes and pages you when the pings stop. That external receiver must live outside the failure domain: a different cloud account, a SaaS heartbeat service, or at minimum a different site. A watchdog that runs on the same VM as Prometheus proves nothing.
Two more checks I add in every setup:
prometheus_notifications_dropped_totalandalertmanager_notifications_failed_totalincreasing. A rotated Slack webhook or an expired SMTP password produces alerts that fire correctly and are delivered nowhere.prometheus_rule_evaluation_failures_totalincreasing. A typo in a recording rule breaks every alert built on top of it, quietly.
Testing it instead of trusting it
promtool test rules lets you feed synthetic series into rules and assert what fires. Write one test per missing-data rule where the series simply stops partway through the input. If the test does not produce an alert, neither will production. Then, once a quarter, stop an exporter on a non-critical host on purpose and time how long it takes for a human to be notified.
Why this matters
Every serious monitoring gap I have dealt with was a silence, not a wrong threshold. Thresholds get tuned because noisy alerts annoy people into fixing them. Missing data annoys nobody, so it survives for weeks until the one incident where it counts. Treat “no data” as a state you alert on, monitor the monitoring from outside its own failure domain, and test the pipeline end to end. It is a small set of rules, and it is the difference between a monitoring system and a dashboard collection.