PromQL

Instant vectors vs. range vectors

http_requests_total alone is an instant vector — the current value of every matching time series, one number each, at the moment the query runs. http_requests_total[5m] is a range vector — every sample recorded for each matching series over the last 5 minutes, not just one value. Functions like rate() consume a range vector and produce an instant vector back out (one rate value per series, computed from that series' samples over the window).

rate(): turning a counter into a rate

A raw counter is nearly useless graphed directly — it only climbs, and it resets to 0 whenever the process restarts, producing a misleading sawtooth rather than a meaningful trend. rate(metric[5m]) computes the per-second average rate of increase over the window, and — this is the part that matters — it specifically detects and correctly handles counter resets.

graph LR v1["t=0: counter=100"] --> v2["t=60s: counter=160"] v2 --> restart["Process restarts"] restart --> v3["t=120s: counter=15<br/>(reset to near-zero)"] v3 --> v4["t=180s: counter=75"]

Tracing rate() across this sequence: from t=0 to t=60s, the counter rose from 100 to 160 — a normal increase, rate ≈ 1/sec. From t=60s to t=120s, the counter appears to decrease (160 → 15) — rate() recognizes a decrease as a restart-induced reset (a counter can only go up during normal operation) and treats the actual increase since the reset as 15 - 0 (not 15 - 160, which would be nonsensical negative), correctly reporting a small positive rate for that interval instead of a meaningless negative one. This reset-handling is the entire reason rate() exists as a dedicated function rather than a simple (end - start) / duration calculation, which would silently produce wrong (negative) numbers across every restart.

irate() is the same idea but uses only the last two data points in the range instead of averaging across the whole window — more responsive to sudden spikes, but noisier; rate() is the standard default for alerting and dashboards specifically because smoothing out noise matters more than millisecond responsiveness for those use cases.

Aggregation: sum, avg, by, without

sum(rate(http_requests_total[5m])) by (status)

Computes the per-second request rate, then sums it, grouped by the status label — collapsing every other label (like handler, method) into one number per status code, across all instances. by (status) keeps only that label in the output; without (status) is the inverse — keep every other label, drop just this one.

Error rate, a canonical pattern combining this with a label filter:

sum(rate(http_requests_total{status=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))

The numerator sums the rate of 5xx responses (=~"5.." is a regex label matcher); the denominator sums the rate of all responses; their ratio is the error rate as a fraction, ready to multiply by 100 for a percentage or feed directly into an alerting rule.

histogram_quantile(): percentiles from bucket data

histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))

Given cumulative bucket counts (le="0.1" → 950 requests, le="0.5" → 990, le="1" → 998, le="+Inf" → 1000, out of 1000 total), this estimates the value below which 99% of observations fall — by finding which bucket boundary the 99th percentile lands between and linearly interpolating within that bucket (it's an estimate, not an exact value, because the raw individual observations aren't stored, only which bucket each fell into). Summing bucket counts across instances before computing the quantile (as shown, via sum(...) by (le)) is exactly the aggregation Summary metrics can't support — this is the concrete PromQL expression of why histograms were the right metric type choice for anything that needs a fleet-wide percentile.

Common pitfall

Using a rate() window shorter than roughly 4x the metric's scrape interval produces noisy or gap-riddled results — if Prometheus scrapes every 15 seconds and a query uses rate(metric[15s]), a single missed or delayed scrape leaves that window with only one data point, which isn't enough to compute a meaningful rate (rate needs at least two samples in the window to measure a change). A common convention is a range at least 4x the scrape interval ([1m] for a 15s scrape interval, [5m] as a common safe default) to stay resilient to an occasional missed scrape without the query going blank.