Skip to main content
Suppose you have a gauge metric and you want to know its maximum value over the past ten minutes. Prometheus provides the max_over_time function for exactly that:
That works fine for gauges. But what if you have a counter metric and you want the maximum rate over the past ten minutes? A naïve attempt might look like this:
This will fail because rate(...) returns an instant vector while max_over_time expects a range vector. Also remember: in rate(metric[10m]), the 10m is the sample range for the rate() calculation (how Prometheus computes the rate), not how far back to query data for aggregation. This is where subqueries solve the problem.
Use subqueries when you need to take an instant-expression (for example, rate(...)) and evaluate that expression across a historical time window at a given resolution so that range-based aggregation functions (like max_over_time, min_over_time, etc.) can operate on a range vector.

Subquery syntax

A Prometheus subquery wraps an instant query and appends a bracketed range and step (resolution). Optionally, you can add an offset.
  • <instant_query>: any instant-vector expression (e.g., rate(...), irate(...), or any metric selector).
  • <range>: how far back to collect data for the subquery (the time window).
  • <resolution>: step between samples returned by the subquery (the query step).
Example: compute the maximum rate of http_requests_total over the last 5 minutes, where rate() uses a 1 minute sample range and the subquery samples every 30 seconds:
  • The inner [1m] tells rate() how to group samples for each instant.
  • The outer [5m:30s] tells Prometheus to evaluate the instant expression for the previous 5 minutes at 30-second intervals, returning a range vector that max_over_time can consume.

Quick comparison: instant vs range vectors

Examples

  1. Trying to nest rate(...) directly inside max_over_time(...) (invalid):
  1. Correct approach using a subquery:
  1. Visualizing how a subquery returns samples:
In the example above:
  • The 1m inside rate() is the sample range used by rate for each evaluated instant.
  • The outer [2m:10s] asks Prometheus to evaluate that rate(...) expression across the past 2 minutes at 10-second intervals, producing a range vector.
Be careful with very small resolution values (the subquery step). High-resolution subqueries can be expensive and may increase query latency and Prometheus load. Choose a resolution appropriate for the precision you need.

Practical example: network transmit bytes

Let’s use a real metric: node_network_transmit_bytes_total (total transmitted bytes on a network interface). To get the current transmission rate:
That yields the current rate per interface (an instant vector), for example:
The image shows the Prometheus query interface in a web browser, where various network-related metrics are listed with prefixes, such as "node_network," and a search bar is used to filter them.
To find the highest rate over the past 5 hours, use a subquery so max_over_time receives a range vector:
  • Inner [1m] is the rate() sample range (how rate computes an instant rate).
  • Outer [5h:30s] requests the last 5 hours of the rate(...) values sampled every 30 seconds.
  • max_over_time(...) then returns the maximum of those sampled rate values.
If you remove max_over_time and just run the subquery:
Prometheus will return a series of sampled points for the last hour at 30-second intervals, which you can inspect or aggregate further.

When to use subqueries

  • Converting an instant-vector expression into a range vector for range aggregations (e.g., max_over_time, avg_over_time).
  • Evaluating rate-like expressions historically at a chosen resolution without using recording rules.
  • Performing windowed calculations over computed instant values.

References

Watch Video