A few months into running Prometheus at home and at work, the most dangerous outage I have seen was not a spike or a threshold breach. It was silence. An exporter crashed, its target dropped out of service discovery, and every alert built on its metrics quietly evaluated to nothing. No series, no comparison, no firing. The dashboard showed “No data”, which nobody looks at at 03:00.
This is a structural property of PromQL, not a bug. An expression like node_filesystem_avail_bytes / node_filesystem_size_bytes < 0.1 returns an empty vector when the underlying series disappear. An empty result means “no alert”. So the more complete your failure, the quieter your monitoring becomes.
Three layers of failure
It helps to separate the ways data can go missing:
- The target is known but unreachable. Prometheus still scrapes it and records
up == 0. This is the easy case. - The target vanished from discovery. A Kubernetes pod is gone, a file_sd entry was removed by a bad deploy, a Consul service deregistered. There is no
upseries at all anymore. - The target is up but a specific metric is gone. The exporter is healthy, but a collector failed, a label changed after an upgrade, or the application stopped emitting a counter because a code path no longer runs.
Most setups only cover the first layer. The other two need explicit handling.
Rules that catch silence
groups:
- name: meta-monitoring
rules:
# Layer 1: known target down
- alert: TargetDown
expr: up == 0
for: 5m
labels:
severity: warning
annotations:
summary: " on is down"
# Layer 2: an entire job disappeared from discovery
- alert: JobMissing
expr: absent(up{job="node"})
for: 10m
labels:
severity: critical
annotations:
summary: "No 'node' targets discovered at all"
# Layer 2b: fewer targets than expected
- alert: JobShrunk
expr: count(up{job="node"}) < 4
for: 10m
labels:
severity: warning
annotations:
summary: "Only node targets present, expected 4"
# Layer 3: a specific metric vanished for a specific host
- alert: MetricMissingPerInstance
expr: |
up{job="node"} == 1
unless on (instance)
node_filesystem_avail_bytes{mountpoint="/"}
for: 15m
labels:
severity: warning
annotations:
summary: " is up but reports no root filesystem metric"
# Layer 3b: series went stale compared to an hour ago
- alert: SeriesDisappeared
expr: |
count by (instance) (node_filesystem_avail_bytes offset 1h)
unless
count by (instance) (node_filesystem_avail_bytes)
for: 15m
labels:
severity: warning
A few notes on these:
absent() only works with fully specified selectors. It returns a single synthetic series when nothing matches, which means it cannot tell you which instance is missing. Use it for “the whole job is gone”, not for per-host checks.
The unless on (instance) pattern is the workhorse for layer 3. It takes the set of targets you know are alive and subtracts those that are emitting the metric you care about. What remains is the list of hosts that are healthy but lying by omission.
The offset 1h comparison catches drift without hardcoding counts. It fires when something that existed an hour ago no longer does. It self-heals after an hour, which is either a feature or a trap depending on how you route it; I send it to a ticket queue, not a pager.
Hardcoded counts like < 4 are ugly but honest. If you know you have four hypervisors, say so. Keep the number next to the inventory source, or generate the rule from it.
Watching the watcher
None of this helps if Prometheus itself or Alertmanager dies. The standard answer is a dead man’s switch: a rule that always fires, routed to an external service that alerts when the heartbeat stops.
- alert: Watchdog
expr: vector(1)
labels:
severity: none
annotations:
summary: "Heartbeat. If this stops arriving, the alerting pipeline is broken."
Route it in Alertmanager to a webhook with a short repeat_interval (a few minutes), pointing at any external heartbeat checker, self-hosted or SaaS, running outside the failure domain you are monitoring. Hosting that checker on the same box as Prometheus defeats the purpose.
Also check prometheus_notifications_dropped_total and alertmanager_notifications_failed_total. A misconfigured SMTP relay or expired chat webhook token turns every alert into a log line nobody reads.
Testing it
promtool test rules supports input series that simply stop. Write a test where a series has values for the first ten minutes and then nothing, and assert that your missing-metric alert fires. If you cannot write that test, you probably do not have that coverage.
Why this matters
Every serious monitoring gap I have dealt with came down to the same pattern: the system that should have told us something was wrong was itself part of what broke. Threshold alerts are written against the failure you imagined. Absence alerts are written against the failure you did not. They are cheap to add, rarely noisy once tuned, and they turn “we found out from a user” into “we found out from the pager”. Treat the monitoring pipeline as production infrastructure with its own health checks, and assume that silence is a signal until proven otherwise.