What Is PromQL? Prometheus Queries Explained Simply
PromQL is the query language that turns Prometheus's raw metrics into answers — rates, averages, alerts. It looks strange at first because it works on time series, not rows. Here's the mental model.
Prometheus stores millions of numbers stamped with a time and a set of labels. PromQL (Prometheus Query Language) is how you ask questions of them: "how many requests per second?", "what's the 95th-percentile latency?", "which instances are down?". It feels alien at first because it doesn't work on rows like SQL — it works on time series. Once that clicks, the rest is small.
Everything is a labelled time series
A metric name plus a set of labels identifies one series:
http_requests_total{job="api", method="GET", status="200"}
The same metric with different labels (method="POST", status="500") is a different series. Querying the bare name returns all series that share it — one number each, right now:
http_requests_total
That's an instant vector: one value per series, at the current instant.
Filter with labels
Narrow to the series you care about by matching labels:
http_requests_total{job="api", status="500"} # only 500s from the api job
http_requests_total{status=~"5.."} # regex: any 5xx
http_requests_total{status!="200"} # everything but 200
Counters only go up — so use rate()
Metrics ending in _total are counters: they only increase and reset to zero on restart. The raw number is meaningless ("4,213,905 requests since boot"). What you want is the per-second rate, over a window:
rate(http_requests_total[5m]) # requests/sec, averaged over the last 5 minutes
The [5m] makes it a range vector (a window of samples), which rate() needs to compute a slope. This is the single most-used pattern in all of PromQL.
Aggregate across series
sum, avg, max collapse many series into fewer. Use by to keep the labels you care about:
sum(rate(http_requests_total[5m])) by (status) # total req/sec, grouped by status code
That turns dozens of per-instance, per-method series into one line per status code — exactly what a dashboard wants.
A few patterns you'll reuse constantly
up == 0 # which targets are down
rate(node_cpu_seconds_total{mode="idle"}[5m]) # idle CPU per core
histogram_quantile(0.95,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le)) # p95 latency
The mental model
- A metric + labels = one time series.
- An instant vector = one value per series, now. A range vector (
[5m]) = a window of samples. - Counters (
_total) → wrap inrate(). Gauges (a temperature, a queue length) → read directly. - Aggregate with
sum(...) by (label)to go from many series to a readable few.
Frequently Asked Questions
What's the difference between an instant vector and a range vector?
An instant vector is one value per series at the current moment (http_requests_total). A range vector is a window of samples over time (http_requests_total[5m]) — functions like rate() need that window to calculate change.
Why do I need rate() on a counter?
Counters only ever increase, so the raw value is just "total since start" — not useful. rate(metric[5m]) converts it into a per-second rate over the window, which is what you actually want to graph or alert on.
Is PromQL like SQL?
No. SQL queries rows in tables; PromQL queries labelled time series and returns vectors of values over time. There are no joins in the SQL sense — you combine series by matching labels and aggregate with by/without.
Stop reading, start building
This pairs with a hands-on BytExplorer course — do it on your own machine and actually keep the skill.