PromQL Basics: Instant Vectors, Range Vectors and rate()

Scenario: A dashboard shows http_requests_total climbing in a straight line forever, which is technically correct but useless for spotting a spike - the raw running total never goes down, so a sudden burst of errors barely dents the slope of the line.

New words, in plain English

Typing just a metric name, like http_requests_total, is the simplest PromQL query there is: it returns an instant vector - the single current value of every time series under that name, right now. Adding a time range in square brackets, like http_requests_total[5m], changes it into a range vector: instead of one value per series, you get every value recorded for each series over the last five minutes.

This distinction matters most for counters - metrics like http_requests_total that only ever climb upward. A raw counter's current value on its own is nearly meaningless ("91,422 requests" - since when?). What you almost always actually want is how fast it is climbing - and that is exactly what rate() calculates: given a range vector, it works out the per-second average rate of increase over that window.

rate() also automatically handles counter resets - if the application restarted and its counter went back to zero, a naive subtraction would show a nonsensical negative rate; rate() detects that and corrects for it.

For a gauge - a metric like memory usage that can go up or down on its own - you generally do NOT use rate(), since a gauge is already meaningful as a raw instant value; you query it directly.

Analogy: A counter is a car's total-distance odometer: it only climbs, and the raw number ("84,213 miles") tells you almost nothing useful by itself. rate() is the speedometer built on top of that same odometer - it looks at how much the odometer changed over a short recent window and reports it as a meaningful speed, correctly accounting for the fact that the number can never legitimately go down (except when a car, or a service, is literally reset to zero).

A worked example

# Instant vector - one current value per series, right now:
http_requests_total
# => http_requests_total{status="200"} 91422
#    http_requests_total{status="500"} 37

# Range vector - every recorded value over the last 5 minutes:
http_requests_total[5m]

# rate() - the actually useful question: how many requests PER SECOND,
# averaged over the last 5 minutes?
rate(http_requests_total[5m])
# => http_requests_total{status="200"} 12.3   (requests/sec)
#    http_requests_total{status="500"} 0.05

# Narrow by label first, exactly like the filing-cabinet drawer from Phase 1:
rate(http_requests_total{status="500"}[5m])

A very common early mistake is graphing a raw counter directly and being confused that the line only ever goes up and barely reacts to an incident. The fix is almost always to wrap it in rate() first. A second common mistake is choosing a range window that is too short relative to the scrape interval - rate() needs at least a couple of real data points inside its window to produce a meaningful number, so a [1m] window against a scrape_interval: 1m job is too tight; a [5m] window is a safe general default.

Tip: A reliable rule of thumb: counters almost always get wrapped in rate() before graphing or alerting on them; gauges almost never do.
Goal: Put this to work in the promgraf-configure-scrape-target lab. Open /labs/prometheus-grafana, pick promgraf-configure-scrape-target, and fix the real broken monitoring stack - a genuine Prometheus server scraping real targets, and a genuine Grafana instance querying it.