The anchored matcher that returns nothing
A regex label matcher written as a substring search. Prometheus anchors it, so it matches the job called exactly "api" and no series come back.
sum by (job) (rate(http_requests_total{job=~"api"}[5m]))Parse a PromQL expression and check the mistakes that still return a number: an anchored label matcher that silently matches nothing, a rate() outside its aggregation, a histogram quantile over raw counters, and a range too short to hold two samples.
Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.
The answer appears here
Paste on the left and press Parse and check. Nothing leaves this tab.
Nothing else to flag.
No formatting problems, and nothing the rules object to. Worth remembering what that covers: this reads the file you pasted, not the account or cluster it will be applied to.
No finding matches that filter.
Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.
A regex label matcher written as a substring search. Prometheus anchors it, so it matches the job called exactly "api" and no series come back.
sum by (job) (rate(http_requests_total{job=~"api"}[5m]))Counters summed before the rate is taken. Counter resets are per series, so one process restarting drags the total down and the correction cannot tell that from a real decrease.
rate(sum(http_requests_total)[5m:])
Bucket series are cumulative since process start, so this is the 99th percentile of every request the process has ever served. It barely moves, which reads as stability.
histogram_quantile(0.99, http_request_duration_seconds_bucket)
rate() computes a slope and needs two points. At the default 15 second scrape interval this window holds one, so the graph is empty and the exporter gets blamed.
rate(http_requests_total[15s])
le is the bucket boundary. Aggregating by job instead of le leaves the quantile function nothing to work with.
histogram_quantile(0.99, sum by (job) (rate(http_request_duration_seconds_bucket[5m])))
These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.
Prometheus anchors matcher regexes as `^(?:...)$`, so `{job=~"api"}` does not match `api-server`. The query returns no series and nothing errors.
Instead:Use `{job=~".*api.*"}` for a substring, or `{job="api"}` for an exact match, which is faster because it uses the index.
Counter resets are per series. Summing first means one process restarting drags the total down, and the reset correction cannot tell that from a genuine decrease. The query runs and returns wrong numbers exactly during a deploy.
Instead:Rate first, aggregate second: `sum(rate(x[5m]))`, never `rate(sum(x)[5m:])`.
Bucket series are cumulative since process start, so the quantile describes every request ever served. It barely moves, which during an incident looks like stability.
Instead:`histogram_quantile(0.99, sum by (le) (rate(x_bucket[5m])))`. The `le` label is the bucket boundary and the function cannot work without it.
rate() needs two samples in the window to compute anything. At a 15s scrape, `[15s]` holds one and returns nothing, and `[30s]` empties whenever a single scrape is missed.
Instead:At least four times the scrape interval, so `[1m]` at 15s. That survives one missed scrape.
`up > 5` filters rather than returning true or false: series that do not satisfy it disappear entirely, which reads as missing data rather than as a low value.
Instead:Add `bool` to get 1 and 0. Leave it off for alerting rules, where an empty result correctly means no alert.
PromQL fails in two ways. A syntax error tells you immediately. Everything else returns a perfectly plausible number that is wrong, and a graph that looks fine is the hardest kind of bug to notice. This parses the expression and checks the second kind.
`{job=~"api"}` matches the job called `api` and nothing else. It does not match `api-server`. Prometheus wraps every matcher regex as `^(?:...)$`, the same rule that makes relabeling surprising, and the symptom is the same: the query returns no series and everything looks correct. Write `.*api.*` for a substring match. If you meant an exact match, use `=` instead, which is faster because it can use the index directly.
{job=~"api"} matches only "api"
{job=~".*api.*"} matches "api-server"
{job="api"} the same as the first, and faster
{job=~""} matches series with NO job label at all Counter resets are per series. `sum(rate(x[5m]))` computes each series' rate, correcting its own resets, and then adds them. `rate(sum(x)[5m:])` adds the counters first, so a single process restarting drags the sum down and the reset correction cannot tell that from a real decrease. The second form parses, runs, and returns numbers that are wrong precisely when a deploy happens, which is when you are looking at the graph.
sum(rate(http_requests_total[5m])) correct
sum by (job) (rate(http_requests_total[5m]))
rate(sum(http_requests_total)[5m:]) wrong, and it runs rate() computes a slope, so it needs two points inside the window or it returns nothing at all. At a 15 second scrape interval, `[15s]` holds one sample and the graph is empty, which sends people to check the exporter. Two or three samples is worse in a way: it computes, and then a single missed scrape empties the window, so the line flickers and reads as an intermittent target. Four times the scrape interval is the usual floor.
Bucket series are cumulative counters that only increase since process start. `histogram_quantile(0.99, http_request_duration_seconds_bucket)` therefore describes every request the process has ever served: it barely moves, and during an incident it looks reassuringly stable. Wrap the buckets in a rate. The other half is that `le` is the bucket boundary and the function cannot work without it, so an aggregation must be `sum by (le)` and not `sum by (job)`.
histogram_quantile(0.99,
sum by (le) (rate(http_request_duration_seconds_bucket[5m]))) `up > 5` does not give you ones and zeros. It drops every series that does not satisfy the comparison. For an alert that is exactly right, because an empty result is no alert. On a graph it means a line vanishes rather than going to zero, which reads as missing data. The `bool` modifier gives you 1 and 0 instead.
It has no metrics and no metadata, so it cannot know whether a name is a counter or a gauge. Where that matters it says so: a rate() over a name that does not end in `_total` is reported as a naming hint rather than as a verdict, because a gauge called `queue_depth_total` would defeat any rule built on the convention. It also does not evaluate anything, so it cannot tell you a query returns no data because the series does not exist. The scrape interval used for the range checks is 15 seconds, which is the Prometheus default; if yours differs, read those two findings against your own number.