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
- PromQL - Prometheus's own query language for asking questions about the metrics it has stored.
- Instant vector - the result of a query that returns exactly one value per time series, at one single moment in time.
- Range vector - the result of a query that returns a whole range of values per time series, over a span of time you specify - written as
metric_name[5m]. - Counter - a metric that only ever goes up (or resets to zero on restart) - like an odometer.
http_requests_totalis a counter. - Gauge - a metric that can go up or down freely, like a thermometer -
memory_usage_bytes,queue_length. - rate() - a PromQL function that turns a raw counter's range of values into a per-second average rate of increase.
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, pickpromgraf-configure-scrape-target, and fix the real broken monitoring stack - a genuine Prometheus server scraping real targets, and a genuine Grafana instance querying it.