prometheus-troubleshooting
Skill0 starsChanged 2 months agoApache-2.0View on GitHub
Fix Prometheus problems - targets down or missing, metrics that disappear, high cardinality and OOMing Prometheus servers, queries that time out, rate() returning nothing, staleness, scrape failures, and alerts that do not fire or fire constantly. Use when Prometheus data is missing, wrong, or expensive, or when a PromQL query returns something unexpected.
In this repository26 other files it gives its agents.
- AGENTS.mdAGENTS.md
- aws-production-debuggingSkill
- github-actions-debuggingSkill
- postgresql-troubleshootingSkill
- container-image-reviewSkill
- docker-debuggingSkill
- incident-triageSkill
- postmortem-analysisSkill
- production-debuggingSkill
- kubernetes-networkingSkill
- kubernetes-production-readinessSkill
- kubernetes-resource-debuggingSkill
- kubernetes-troubleshootingSkill
- linux-network-debuggingSkill
- linux-performanceSkill
- linux-troubleshootingSkill
- dns-troubleshootingSkill
- http-debuggingSkill
- tls-debuggingSkill
- observability-debuggingSkill
- slo-designSkill
- secrets-management-reviewSkill
- technology-jobSkill
- terraform-reviewSkill
- terraform-safe-changesSkill
- terraform-troubleshootingSkill
The file
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 3 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 52
Query the raw series: before the aggregation. A broken rate() on a healthy counter is a query problem, not a data problem.
- Very specific: line 77
If up returns no series at all, the target is not in the configuration — a service discovery or relabelling problem, not a scrape failure.
- Distinctive: line 110
Check the generated configuration rather than reasoning about the CRDs:
- Very specific: line 241
All PromQL queries, /targets, /api/v1/status/config, and reading the generated scrape configuration.
- Distinctive: line 287
Verify all four layers, because fixing one commonly reveals the next:
- Very specific: line 298
Better: Check up first. An empty graph much more often means a scrape or query problem than a dead service — and treating a monitoring …
- Distinctive: line 302
Bad: Add a label so we can break the metric down by user.
- Very specific: line 307
Better: If the process dies, that series disappears and the alert silently stops firing.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Prometheus Troubleshooting
Establish whether the problem is collection, storage, query, or alerting — they look identical from a blank dashboard.
Purpose
Use this when:
- A target is
DOWNor missing from/targets. - A metric that used to exist returns no data.
- A PromQL query returns nothing, or something implausible.
- Prometheus is OOMing, restarting, or using far more memory than expected.
- An alert did not fire during an incident, or fires constantly.
Do not use this when:
- Prometheus is fine and you are debugging the application it monitors — use
observability-debugging. - You are designing alerts and SLOs rather than fixing them — use
slo-design.
What this skill assumes: access to the Prometheus UI or API, and ideally the scrape configuration. Many of these checks are answered by Prometheus's own metrics about itself.
Operating Procedure
- Localise the layer. Is the metric being scraped, stored, queried, or alerted on? Check in that order — each layer depends on the one before.
- Check the target first.
/targetsandupanswer most "missing metrics" reports in seconds. - Query the raw series before the aggregation. A broken
rate()on a healthy counter is a query problem, not a data problem. - Use Prometheus's own metrics to diagnose Prometheus.
prometheus_tsdb_*andscrape_series_addedare the real evidence. - Fix at the right layer. A missing label is fixed in relabelling; a missing metric is fixed in the application.
- Verify with the same query that failed.
An empty query result is Observed but ambiguous: it means "no matching series", never "the value is zero". Never report an empty result as a zero.
Investigation
Step 1 - is it being scraped
up{job="<job>"}
up == 0
scrape_samples_scraped{job="<job>"}
scrape_duration_seconds{job="<job>"} > 10up is synthesised by Prometheus for every scrape: 1 for success, 0 for failure. If up is 0, no
metric from that target can exist and nothing else in this skill matters yet.
If up returns no series at all, the target is not in the configuration — a service discovery
or relabelling problem, not a scrape failure. These are different faults that produce the same
empty dashboard.
Then read /targets in the UI for the exact error:
| Error | Cause |
connection refused | Nothing listening on the metrics port |
context deadline exceeded | The /metrics endpoint is slower than scrape_timeout |
server returned HTTP status 404 | Wrong metrics_path |
x509: certificate signed by unknown authority | TLS trust for the scrape target |
out of bounds / sample limit exceeded | The target exposes more series than sample_limit allows |
A /metrics endpoint that takes longer than scrape_timeout produces intermittent gaps rather
than a clean failure — sawtooth graphs with no obvious error.
Step 2 - service discovery and relabelling
count by (job) (up)
count by (job, namespace) (up)Compare against what you expect to exist. In Kubernetes, when a ServiceMonitor or PodMonitor exists but no target appears, work through this order — it is almost always one of them:
- The
selectordoes not match the Service's labels. - The Service port is not named, or the name does not match
port:in the monitor. namespaceSelectorexcludes the namespace.- The Prometheus resource's own
serviceMonitorSelectordoes not select your monitor. This one is invisible from the monitor's side and catches people repeatedly.
Check the generated configuration rather than reasoning about the CRDs:
kubectl get secret -n <ns> prometheus-<name> -o jsonpath='{.data.prometheus\.yaml\.gz}' \
| base64 -d | gunzip | grep -A10 '<job-name>'This is the configuration Prometheus actually loaded. If your job is not in it, the problem is entirely in the operator's selectors.
Step 3 - the metric itself
{__name__=~"<partial_name>.*"}
count({__name__=~".+", job="<job>"}) by (__name__)Confirm the metric name, including the suffix. _total on counters and _bucket/_sum/_count
on histograms are real parts of the name, and querying http_requests when the metric is
http_requests_total returns nothing with no error.
rate(http_requests_total[5m])rate() returning empty on a series that clearly exists has three standard causes:
- Fewer than two samples in the window. With a 60s scrape interval,
[1m]may contain one point. Rule of thumb: the range must be at least 4x the scrape interval. - Applied to a gauge.
rate()is only defined for counters; on a gauge it produces nonsense or nothing. - The series is stale. Prometheus marks a series stale 5 minutes after the last sample, and it disappears from instant queries.
Step 4 - cardinality and memory
topk(10, count by (__name__) ({__name__=~".+"}))
topk(10, count by (job) ({__name__=~".+"}))
sum(scrape_series_added) by (job)
prometheus_tsdb_head_seriesprometheus_tsdb_head_series is the total number of active series and the primary driver of
Prometheus memory. Rough guidance: budget a few KB of RAM per active series, so a million series
needs several GB before query overhead.
High cardinality comes from putting unbounded values in labels. The usual offenders, in order:
| Label containing | Why it explodes |
| User ID, request ID, trace ID | Unbounded by definition |
| Full URL path with IDs | /users/12345/orders/67890 — one series per request |
| Pod name | Every deploy creates a new set of series |
| Error message text | Unbounded |
| Timestamp or epoch | Unbounded |
A metric with 20 labels of 10 values each is 10^20 possible series. Cardinality is multiplicative, which is why it goes from fine to fatal with one added label.
sum(rate(prometheus_target_scrapes_exceeded_sample_limit_total[5m]))Non-zero means a target is being rejected for exposing too many series — a protection working correctly, and a signal to fix the application's labels.
Step 5 - queries and alerts
prometheus_engine_query_duration_seconds{quantile="0.99"}
ALERTS{alertstate="firing"}
ALERTS_FOR_STATEFor an alert that did not fire, check in this order:
- Does the expression return data now? Run it in the UI.
- Was it returning data at the time? An alert on
rate(errors[5m]) > 0.05never fires if the errors metric disappears entirely when the service dies — an absent series is not greater than anything. This is the most dangerous alerting bug there is, and the fix is anabsent()orup == 0alert alongside it. - Is
for:longer than the incident lasted? - Did Alertmanager receive it and suppress it — inhibition, silence, or grouping?
curl -sS http://<alertmanager>:9093/api/v2/silences | jq -r '.[] | select(.status.state=="active") | .comment'A forgotten silence from a previous incident is a common reason an alert "did not fire".
Decision Tree
Metric or alert problem
├── up == 0 → scrape failing → read the error on /targets
├── up has no series → target not discovered → selectors and relabelling
├── up == 1 but metric missing
│ ├── Wrong name or suffix → check with __name__ regex
│ ├── Not exposed by the app → curl the /metrics endpoint directly
│ └── Dropped by metric_relabel_configs → read the generated config
├── Series exists, rate() empty
│ ├── Range < 4x scrape interval → widen the range
│ ├── Metric is a gauge → use delta() / deriv() or no function
│ └── Series stale (>5 min old) → the target stopped exposing it
├── Prometheus OOM / slow
│ ├── head_series growing → cardinality → find the metric and the label
│ └── Query duration high → unbounded queries or too-long ranges
└── Alert did not fire
├── Expression returns nothing when the service is down → needs absent()/up==0
├── for: longer than the event
└── Alertmanager silence, inhibition, or groupingCommon Causes
| Cause | Signal | Fix | Risk |
| Unnamed Service port | ServiceMonitor selects nothing | Name the port; match it in the monitor | mutating |
serviceMonitorSelector mismatch | Monitor exists, job absent from generated config | Label the monitor to match the Prometheus resource | mutating |
| Scrape timeout | Sawtooth gaps, context deadline exceeded | Speed up /metrics or raise the timeout | mutating |
| High cardinality label | head_series climbing, Prometheus OOM | Remove the label; drop with metric_relabel_configs | mutating |
rate() on too short a range | Empty result | Range >= 4x scrape interval | read-only |
| Alert on a metric that disappears | Silent during the outage it was written for | Pair with absent() or up == 0 | mutating |
| Forgotten silence | Alert firing internally, no notification | Expire the silence; set expiry on future ones | mutating |
| Counter reset not handled | Impossible spikes | Use rate()/increase(), never raw differences | read-only |
Safety
Read-only
All PromQL queries, /targets, /api/v1/status/config, and reading the generated scrape
configuration. Note that a very expensive query — an unbounded regex over a long range — can itself
put a strained Prometheus under enough load to affect alert evaluation. Bound your queries during
an incident.
Potentially disruptive
- Reloading Prometheus configuration (
POST /-/reloador SIGHUP) — brief, but a bad configuration fails to load and leaves the previous one running, which is confusing under pressure. Validate withpromtool check configfirst. - Restarting Prometheus — on a large TSDB, replaying the WAL can take minutes, during which there is no alerting at all. Never restart Prometheus during an incident without telling the incident channel that alerting is offline.
- Adding a
metric_relabel_configsdrop rule — stops collection of that metric immediately and permanently. The historical data remains; nothing new is added. Confirm nothing alerts on it. - Creating an Alertmanager silence — suppresses notifications. Always set an explicit expiry; an indefinite silence is how the next incident goes unnoticed.
- Changing retention or storage settings — restarts the server and can delete data outside the new retention window.
Destructive
- Deleting time-series data via the admin API (
/api/v1/admin/tsdb/delete_series, followed byclean_tombstones) — permanently removes history. It is disabled unless--web.enable-admin-apiis set, which is a deliberate choice. Required first: confirm the series selector matches only what you intend (run it as a normal query and check the count), confirm nobody needs the history for an ongoing investigation, and get explicit approval. - Deleting the Prometheus PersistentVolumeClaim to "reset" a broken server — destroys all historical metrics. Required first: confirm remote-write or a long-term store holds the data, or accept the permanent loss in writing. A Prometheus that will not start is nearly always fixable by other means; check disk space and WAL corruption messages before considering this.
Verification
Before: 0/12 targets up for job=api; dashboards empty; no alerts firing
After: 12/12 targets up; rate(http_requests_total[5m]) returns 12 series; test alert firedcount(up{job="<job>"} == 1)
rate(<metric>[5m])
prometheus_tsdb_head_seriesVerify all four layers, because fixing one commonly reveals the next:
- Scrape —
up == 1andscrape_samples_scrapednon-zero. - Storage — the series is queryable over a range, not just instantaneously.
- Query — the exact expression from the dashboard or alert returns what you expect.
- Alert — force the condition in a non-production environment and confirm the notification actually arrives. An alert rule that evaluates correctly but routes nowhere is not working.
Anti-Patterns
Bad: The graph is empty, so the service is down.
Better: Check up first. An empty graph much more often means a scrape or query problem than a
dead service — and treating a monitoring failure as an outage wastes the first ten minutes of an
incident.
Bad: Add a label so we can break the metric down by user. Better: Cardinality is multiplicative and unbounded labels take down the Prometheus server for everyone. Per-user breakdowns belong in logs or traces.
Bad: Alert on rate(http_errors_total[5m]) > 0.05.
Better: If the process dies, that series disappears and the alert silently stops firing.
Pair every such alert with absent() or up == 0, or express it as a ratio with a denominator
that also disappears.
Bad: Restart Prometheus when data looks wrong. Better: WAL replay can take minutes with no alerting. Diagnose first; a restart rarely fixes a scrape configuration problem anyway.
Bad: Increase scrape interval to reduce load.
Better: That reduces resolution everywhere and breaks rate() over short ranges. Reduce
cardinality instead — it is nearly always where the cost is.
Bad: Silence the noisy alert. Better: A noisy alert is either mistuned or reporting something real that nobody is fixing. Silencing it indefinitely removes the signal permanently. Fix the threshold, or delete the alert on purpose.
Composition
observability-debugging— the metrics work and the question is what they mean.slo-design— designing alerts that are worth firing.kubernetes-troubleshooting— Prometheus itself is a pod and can fail like any other.kubernetes-resource-debugging— Prometheus OOMing because of its memory limit.
References
- Prometheus: Querying basics
- Prometheus: Instrumentation best practices — including what not to put in a label.
- Prometheus Operator: troubleshooting ServiceMonitors
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

