← All runbooks in Observability
Runbook: Investigate a Missing Metric
1 · Prerequisites
Confirm every item is in place before any state change.
- Missing Metrics Anatomy
- Exporter Down
- Network Block
- Scrape Config Error
- Relabel Drops
- Metric Relabeling
- Query Wrong
- Read access to the Prometheus HTTP API on the server that is actually backing the failing panel
- Shell access, or someone who has it, on the host that runs the exporter
- Read access to the repository that owns prometheus.yml, and the last 24 hours of changes to it
2 · Pre-checks
Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.
- · The report is turned into three concrete things: a metric name, a job, and the Prometheus server the reporter was querying. The two halves of an HA pair disagree about a metric for legitimate reasons, and diagnosing the wrong one wastes the entire investigation.
- · The target health is established first with
up{job="JOBNAME"}. A value of 0, or no series at all, means this is a missing-TARGET incident and belongs in that runbook - the two share a symptom and no diagnostic steps. - · It is established whether this metric EVER existed on this server. Never-existed points at the exporter contract, a relabel rule or the query. Existed-until-Tuesday points at a change, and the change window is faster to read than any API.
- ·
prometheus_config_last_reload_successfulis confirmed to be 1. If it is 0, the file on disk is not the configuration being run, and every conclusion drawn from reading that file will be wrong. - · The last 24 hours of change are read before a theory is formed: a scrape-config merge, a new relabel rule, an exporter version bump, a deploy that renamed a metric.
- · It is established whether the metric is missing for ONE instance or for every instance in the job. One instance is an exporter or host problem; the whole job is configuration, relabelling or the metric name itself.
- · Nobody has restarted Prometheus or the exporter. Restarting the exporter destroys the evidence for why it stopped, and restarting Prometheus fixes nothing that was ever caused by the exporter.
3 · Procedure
Execute each step in order. Verify the expected output of a step before moving to the next.
- 1Split the problem on target health before anything else. Query
up{job="JOBNAME"}. If it is 1, the scrape is succeeding and the metric is being lost somewhere between the exporter body and the panel - continue here. If it is 0 or absent, stop: this is the missing-target path, and none of the steps below apply. - 2Link 1 - is the exporter process alive and bound? On the exporter host:
systemctl statusordocker ps -a, andss -tlnpfor the port. This is the first check because it is the cause in roughly seven of ten missing-metric incidents, and it costs thirty seconds. An exited container with an old timestamp bounds the gap exactly. - 3Link 2 - can Prometheus reach the exporter? From the Prometheus host, not from your laptop:
curl --connect-timeout 5against the metrics endpoint. Connection refused means the host is up and the port is closed; a timeout means the path is dropping packets. ThelastErrorfield on the target names which one occurred on the last scrape. - 4Link 3 - does Prometheus believe it should scrape this target? Read
/api/v1/targetsfor the job and confirm the instance appears at all. A target absent from the active list was never created, which is a scrape-config or discovery fault rather than a metric fault. - 5Link 4 - run the one comparison that catches the silent class.
curlthe exporter body for the metric name, then query/api/v1/series?match[]=METRIC. Present in the body and absent from the index is a relabel drop, and it is the only evidence that failure shape ever leaves. - 6Distinguish the two relabel stages, because they have different blast radii.
relabel_configsacts on TARGETS before the scrape - a drop there removes every sample from that target.metric_relabel_configsacts on SAMPLES after a successful scrape - a drop there removes one metric while everything else on the target stays healthy. - 7Check the anchoring on every regex you are about to blame. Prometheus relabel regexes are fully anchored at both ends:
regex: proddoes not matchproduction. This single behaviour is the most common relabel bug, and it is invisible because a non-matching rule is a valid no-op that logs nothing. - 8Link 5 - is it a derived metric with a silent rule behind it? If the metric comes from a recording rule, its inputs being present while it is absent points at the rule, not at the pipeline. Check whether the rule group loaded and is evaluating before looking anywhere else.
- 9Link 6 - only now, the query. If
/api/v1/seriesreturns the metric, the pipeline is healthy and the expression is the bug. List the actual labels and the actual values from the series output and compare them character by character against the panel. Do not rewrite the query before running that comparison. - 10Fix at the link the evidence named, in its reversible form. Restart or repair the exporter at link 1; fix the firewall or route at link 2; correct the scrape config at link 3; correct or remove the relabel rule at link 4; fix the rule at link 5; fix the panel or alert expression at link 6.
- 11Validate a configuration fix offline before reloading.
promtool check config, thenpromtool check service-discoveryfor the affected job to see the final label set the rules produce. A reload that fails leaves the OLD configuration running and the file on disk telling a different story. - 12Reload, then prove the reload took. Confirm
prometheus_config_last_reload_successfulis 1 and read/api/v1/status/configfor the section you changed. "I edited the file" and "Prometheus is running that file" are separate facts. - 13Bound and record the gap. From when to when was the metric absent, for which instances, and does anything downstream depend on it - an SLO, an alert, a capacity report. A metric that was missing for six hours has left a hole in every derived series computed over that window.
4 · Verification
Confirm the procedure actually fixed the problem.
- ✓
up{job="JOBNAME"}is 1 for every expected instance, confirming the split at step 1 still holds after the fix. - ✓
/api/v1/series?match[]=METRICreturns a non-zero number of series - the metric is in the index, not merely in the exporter body. - ✓The exporter body and the series list now agree. This is the same comparison as step 4 and it is the only one that proves no relabel rule is still eating the metric.
- ✓The original panel or alert expression returns data, run from the original dashboard against the original Prometheus server. A corrected expression in a curl proves the metric exists, not that the panel works.
- ✓
prometheus_config_last_reload_successfulis 1, and/api/v1/status/configshows the section that was changed. A silently rejected reload is the commonest way a config fix appears to do nothing. - ✓
scrape_samples_scrapedfor the affected target is back to its normal value rather than merely non-zero. A relabel drop that removed one metric out of hundreds barely moves this number, so compare against the pre-incident value. - ✓The metric is present for EVERY instance in the job, not just the one that was investigated. Fixing one host and declaring victory is how the same incident is reopened the following week.
- ✓Any alert rule or recording rule that consumes the metric has evaluated at least once since the fix and produced a result.
- ✓The gap is recorded with start, end and affected instances, and anyone who owns a derived series over that window has been told. Rates and SLO burn computed across the hole are wrong and will stay wrong.
5 · Rollback
If verification fails, undo the procedure in reverse order.
- ↶Links 1 to 6 are read-only diagnosis. Nothing needs rolling back until a fix is applied, which is the reason the chain is walked before anything is changed.
- ↶To undo a scrape-config or relabel change: revert the commit, reload, and confirm from
/api/v1/status/configthat the running configuration matches - not from the file on disk, which is not evidence of what is running. - ↶To undo a panel or alert expression change: revert the dashboard JSON or the rule file. This is the cheapest reversal available and covers most link-6 resolutions.
- ↶An exporter restart cannot be undone, and whatever the exporter was going to tell you about why it stopped is gone with it. Capture its logs before restarting, or accept that the cause will remain unknown.
- ↶Removing a
metric_relabel_configsdrop rule brings the metric back but also brings back its cost. If the rule was dropping a high-cardinality metric deliberately, removing it can push the head series count into a memory problem - check what it was dropping before deleting it. - ↶Data missing from the gap cannot be recovered. Prometheus has no backfill for a scrape that never happened; the hole is permanent and belongs in the incident record rather than in a fix.
- ↶If a metric was re-added by widening a
keeprule, review the widened rule before leaving it. A regex loosened at 03:00 to solve one metric routinely admits several thousand series nobody costed.
6 · Escalation
When the runbook isn't enough, contact:
- · Escalate to the team that owns the exporter host when link 1 is the fault and you have no shell there. The platform is healthy and no Prometheus-side change will fix a process that is not running.
- · Escalate to the network or security-group owner when link 2 shows a timeout rather than a refusal. A silent drop is a policy decision somewhere, and it usually affects more than the one target that was noticed.
- · Escalate to whoever owns the scrape configuration before widening a
keepregex or removing adroprule. Those rules are frequently a cardinality control, and undoing one is a capacity decision with a budget owner. - · Escalate to the service owner when the metric name changed because their exporter was upgraded. The dashboard fix belongs to them, and every other panel using the old name is broken too and has not been noticed yet.
- · Escalate immediately if the missing metric is an SLO input or an alert condition. An alert whose query returns no data does not fire, which means the absence has been silently masking whatever it was meant to detect.
- · Escalate to security if a metric disappeared with no corresponding change record. Monitoring going quiet with nobody able to say why is also what a compromised host looks like.
An empty panel is not a diagnosis. Six independent things sit between an exporter producing a number and a graph drawing it, each fails on its own, and every one of them produces the identical empty panel.
1. Exporter 2. Network 3. Scrape config
process path (prometheus.yml,
bound? reachable? discovery)
| | |
+----------------+--------------------+
|
v
6. Query 5. TSDB 4. Relabel
PromQL, series relabel_configs
labels, present? metric_relabel_configs
panel
The operator’s instinct is to start at link 6, because that is the link that is visible on screen and the only one they can edit. It is also the least likely to be at fault. Roughly seven in ten of these incidents end at link 1, which costs thirty seconds to check.
Step 0 - The split that decides whether this is your runbook
Before anything else, ask whether the target is healthy.
PROM=http://prometheus.monitoring.svc:9090
curl -s -G "$PROM/api/v1/query" \
--data-urlencode 'query=up{job="node"}' \
| jq -r '.data.result[] | "\(.metric.instance) up=\(.value[1])"'node-1.internal:9100 up=1
node-2.internal:9100 up=1Illustrative output
up == 1 means Prometheus connected, scraped and parsed a response.
The pipeline works; something between that response and the panel is
losing your metric, and the rest of this runbook applies.
up == 0, or no series at all, is a different incident: the target is
down or was never created. Stop here and use the missing-target
runbook. The two symptoms are identical on a dashboard and the two
procedures share nothing.
When this runbook applies, and when it does not
It applies when the target is healthy and a metric you expect from it is not queryable.
It does not apply when:
- The target is down or absent. That is the missing-target path.
- The metric exists but the numbers look wrong. That is a
correctness question - a
rateover a counter reset, a bad aggregation, a unit mismatch - not an absence. - The metric is missing only outside retention. It aged out. That is a retention conversation.
- Everything from every job is missing. That is a Prometheus availability incident, and it starts with whether the process is running and whether its disk is full.
Blast radius
The whole chain walk is read-only. The risk badge reflects the remediation, and two of the fixes touch shared configuration.
| Action | Reversible? | What it costs if wrong |
|---|---|---|
| Reading targets, series, exporter bodies | n/a | Nothing |
| Fixing a panel or alert expression | Yes | Scoped to that panel |
| Restarting an exporter | Partly | You lose the reason it stopped |
Editing metric_relabel_configs | Yes, on revert | A widened rule can admit thousands of series |
Editing relabel_configs | Yes, on revert | Affects every metric from those targets |
| Reloading Prometheus | Yes | A rejected reload silently keeps the old config |
Link 1 - The exporter, which is usually the answer
# On the exporter host.
systemctl status node_exporter --no-pager | head -12
ss -tlnp | grep :9100
docker ps -a --filter name=node_exporter --format '{{.Names}} {{.Status}}'● node_exporter.service - Prometheus Node Exporter
Active: inactive (dead) since Mon 2026-08-17 20:41:09 UTC
node_exporter Exited (137) 6 hours agoIllustrative output
An exit code of 137 is a kill signal, which in practice usually means the process was terminated for using too much memory. Read the journal for the reason before restarting: the restart is the fix, and it is also the thing that removes the evidence for why it was needed.
An exit timestamp bounds the gap exactly. That number belongs in the incident record, because every rate and every SLO computed across that window is wrong.
Link 2 and Link 3 - Reachability, and whether the target exists at all
Test reachability from the Prometheus host. Your laptop is on a different network with different rules, and a successful curl from it proves nothing about the scrape.
curl --connect-timeout 5 -s http://node-1.internal:9100/metrics | head -5
curl -s "$PROM/api/v1/targets?state=active" \
| jq '.data.activeTargets[]
| select(.labels.job=="node")
| {instance: .labels.instance, health: .health, lastError: .lastError}'# HELP node_cpu_seconds_total Seconds the CPUs spent in each mode.
# TYPE node_cpu_seconds_total counter
node_cpu_seconds_total{cpu="0",mode="idle"} 371829.41
{
"instance": "node-1.internal:9100",
"health": "up",
"lastError": ""
}Illustrative output
lastError is the highest-signal field in this whole subsystem, and it
distinguishes causes that otherwise look identical:
lastError says | Link | Meaning |
|---|---|---|
| connection refused | 2 | Host is up, nothing is listening on that port |
| context deadline exceeded, i/o timeout | 2 | Packets are being dropped somewhere in the path |
| no such host | 3 | DNS or a typo in the configured address |
| HTTP 404 | 3 | Wrong metrics_path |
| empty, health up | 4 or later | The scrape worked. The problem is downstream |
Link 4 - The one comparison that catches the silent class
A relabel drop is the failure shape with no evidence anywhere: up is
1, lastError is empty, scrape duration is normal, and the metric is
simply not in the index. Nothing logs it. One comparison finds it.
# What the exporter actually returned.
curl -s http://node-1.internal:9100/metrics \
| grep -c '^node_cpu_seconds_total'
# What Prometheus actually stored.
curl -s -G "$PROM/api/v1/series" \
--data-urlencode 'match[]=node_cpu_seconds_total' \
| jq '.data | length'8
0Illustrative output
Eight lines in the body and zero series in the index is a relabel drop. That is the entire diagnosis, and it took two commands.
The two relabel stages are not interchangeable, and knowing which one you are looking at tells you how wide the damage is:
| Stage | Acts on | A drop here removes | Symptom |
|---|---|---|---|
relabel_configs | Targets, before the scrape | Every sample from that target | Job looks empty; up never goes to 0 because no scrape happens |
metric_relabel_configs | Samples, after the scrape | One metric from a healthy scrape | One panel dark, target perfectly healthy |
Confirm the theory offline, against the real label values, rather than by reading the YAML and reasoning about it:
promtool check config /etc/prometheus/prometheus.yml
promtool check service-discovery /etc/prometheus/prometheus.yml node \
| jq '.[] | {discovered: .discoveredLabels, final: .labels}'discoveredLabels is what discovery produced; labels is what
survived the rules. An empty final set is a dropped target. This is
also the check to run before merging a relabel change, which is where
it belongs.
Link 5 and Link 6 - Derived metrics, then the query
If the metric comes from a recording rule, a healthy pipeline with a missing output points at the rule: check that its group loaded and is evaluating before suspecting anything upstream, because its inputs being present is exactly the evidence that the pipeline is fine.
Only when /api/v1/series returns the metric is the query the suspect.
curl -s -G "$PROM/api/v1/series" \
--data-urlencode 'match[]=node_cpu_seconds_total' \
| jq '.data[0]'
curl -s -G "$PROM/api/v1/series" \
--data-urlencode 'match[]=node_cpu_seconds_total' \
| jq -r '[.data[].mode] | unique[]'{
"__name__": "node_cpu_seconds_total",
"cpu": "0",
"instance": "node-1.internal:9100",
"job": "node",
"mode": "idle"
}
idle
iowait
system
userIllustrative output
Now compare that against the panel. The common causes are a metric renamed by an exporter upgrade, a label value that is not what the author assumed, a case mismatch, or an aggregation that groups by a label not present on every series in the range.
The discipline is to run the comparison before editing. An operator who rewrites the expression first gets a second empty panel and no new information.
Fix, reload, and prove the reload took
promtool check config /etc/prometheus/prometheus.yml
kill -HUP "$(pidof prometheus)"
curl -s -G "$PROM/api/v1/query" \
--data-urlencode 'query=prometheus_config_last_reload_successful'
curl -s "$PROM/api/v1/status/config" | jq -r '.data.yaml' | head -30This is the step people skip. A rejected reload is silent from the outside: Prometheus keeps running the previous configuration, the file on disk shows your change, and the metric stays missing for a reason that has nothing to do with the fix you made.
Common patterns
| What you see | Link | Where to look |
|---|---|---|
up == 0, lastError connection refused | 1 | The exporter process, then its journal for why |
Container Exited (137) | 1 | Memory limit; the exit time bounds the gap |
up == 0, lastError i/o timeout | 2 | Firewall or security group, from the Prometheus host |
Target absent from /api/v1/targets | 3 | Scrape config or discovery - not a metric problem |
| lastError HTTP 404 | 3 | metrics_path |
| Body has the metric, index has zero series | 4 | A relabel drop. The only evidence it leaves |
Whole job dark, up never went to 0 | 4 | relabel_configs dropped the targets pre-scrape |
| One metric dark, target healthy | 4 | metric_relabel_configs |
| Rule matched nothing and logged nothing | 4 | Anchored regex |
| Derived metric missing, inputs present | 5 | The recording rule group |
Metric in /api/v1/series, panel empty | 6 | The expression - compare labels, do not rewrite |
| Config edited, nothing changed | any | prometheus_config_last_reload_successful |
Prevention
Three controls remove most of this class.
The first is alerting on up == 0 per job with a short for clause,
and separately on the count of targets in a job dropping. The second
catches the relabel case, where up never goes to 0 because no scrape
ever happens - the failure that has no alert unless you write it.
The second is promtool check service-discovery in CI on every change
to a scrape config, with the final label set diffed against the
previous run. Every silent relabel drop in production passed a
promtool check config that only ever validated the syntax.
The third is a unit test for any alert rule that depends on a metric name. A rule referencing a metric that no longer exists is silent rather than broken, and silence is indistinguishable from health right up until the moment it matters.