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.

BytExplorer 6 min read July 18, 2026

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 in rate(). 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.

Put it into practice

Stop reading, start building

This pairs with a hands-on BytExplorer course — do it on your own machine and actually keep the skill.

More in Core Concepts