Skip to main content
This lesson explains Prometheus subqueries and when to use them — specifically, how to apply range-vector functions (for example, max_over_time) to the output of instant-vector expressions (for example, rate). Subqueries let you evaluate an instant-vector expression repeatedly over a time window and produce a range vector that range functions can consume. Why this matters: rate(...) and other instant-vector functions return a single sample per series at each evaluation time. Range-vector functions such as max_over_time(...) require a range vector (multiple samples across time). Subqueries bridge the gap.
Subqueries let you take any expression and evaluate it over a range with a given sampling step, producing a range vector that can be consumed by range-vector functions like max_over_time.

Quick motivating examples

  • For a gauge metric, you can get the maximum over the last 10 minutes directly:
  • For a counter metric where you want the maximum of the rate, the following is incorrect and will fail:
Why it fails: rate(...) is an instant vector; max_over_time(...) expects a range vector. The 10m inside rate(...) is the per-sample lookback window used to compute the rate, not the range for max_over_time.

Subquery syntax and semantics

General form:
  • The inner expression (for example rate(http_requests_total[1m])) is evaluated to produce a time series.
  • The [...] after that turns the evaluated expression into a range vector by specifying:
    • <range> — how far back each subquery sample’s range should extend (e.g. 5m, 1h).
    • <resolution> — the time step between subquery samples (e.g. 30s, 10s).
  • Optionally, offset <duration> shifts the whole subquery back in time.
Example:
Interpretation:
  • rate(http_requests_total[1m]) — compute a per-second rate at each evaluation point using a 1-minute lookback.
  • [5m:30s] — for each output time of the subquery, collect the previous 5 minutes of those rate samples, sampled every 30 seconds, producing a range vector per series.
You can then pass the resulting range vector into range functions:
This returns the maximum rate observed during the last 5 minutes, using samples spaced 30 seconds apart where each sample was computed using a 1-minute rate window.
Remember: the inner duration (e.g. 1m in rate(...)) is the per-sample lookback used to compute that instant value. The subquery range (e.g. 5m) is how far back each range vector spans. These are independent parameters and serve different purposes.

Subquery components — quick reference

Visualizing what a subquery returns

A subquery produces multiple sampled values per series (a range vector). For example:
Might yield output like:
  • 2m is the range window for each sampled range vector (the subquery range).
  • 10s is the resolution (the spacing between subquery samples).
  • 1m inside rate(...) is the per-sample lookback window used when computing each rate sample.

Practical example: maximum network transmit rate

Attempting this will error because rate(...) yields an instant vector:
Instead, use a subquery to create a range vector that max_over_time can consume:
Breakdown:
  • rate(node_network_transmit_bytes_total[1m]) — compute per-second transmit rate with a 1-minute lookback.
  • [5h:30s] — collect 5 hours of those rate samples, sampled every 30 seconds.
  • max_over_time(...) — compute the maximum per-series rate across that 5-hour window.
Running only the subquery (without max_over_time) evaluates the current rates sampled across the specified range. For example:
Might present series current rates such as:
Shortening the subquery range focuses the result on a smaller time window:
Example output:

Summary and best practices

  • Use range-vector functions like max_over_time directly for gauge metrics (they accept range vectors).
  • For counters where you want to aggregate over the values produced by rate(...) (or other instant-vector expressions), wrap the inner expression in a subquery so the outer function receives a proper range vector:
    • Pattern: max_over_time(rate(metric[rate_window])[query_range:query_step])
  • Choose the inner window (e.g. 1m in rate(...)) to match the resolution and noise tolerance you need for per-sample calculations.
  • Choose the subquery range and resolution ([range:step]) to capture the retention and sampling granularity required for the outer aggregation.
  • Subqueries are a compact and powerful way to combine per-sample computations with range-based aggregations in PromQL.
Further reading and references:

Watch Video