Skip to main content
RunBook Academy

ObservabilityXII · PromQL FoundationsPromQLFoundations

Instant and Range Vectors

Foundation⏱ ~22 minbashcurl

What you'll learn

  • Define an instant vector and a range vector in PromQL terms
  • Recognise that the [duration] suffix selects a range, not a value
  • Identify which functions and operators accept which vector shape
  • Validate the difference between the two vectors against /api/v1/query

Prerequisites

Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13

Not yet marked complete on this device.

A panel reads “No data”. The query in the panel reads node_cpu_seconds_total[5m]. The query is well-formed; the expression browser accepts it; the HTTP API returns a result — the shape of the result is the problem. Grafana’s time-series panel cannot plot a range vector. The panel wants an instant vector at each pixel; the query produced a matrix of samples.

This lesson is the distinction between the two vector types that dominate PromQL. Once the shape is clear, every other rule in this part falls out: which functions accept which inputs, why [5m] is only legal directly after a metric selector or a subquery, and why an empty range vector means “the metric had no samples in that window”, not “the metric does not exist”.

What they are

An instant vector is a set of time series, each with exactly one sample at the evaluation timestamp t. The result of node_cpu_seconds_total against /api/v1/query is an instant vector: for every (instance, cpu, mode) combination, one sample at t.

A range vector is a set of time series, each with a matrix of samples spanning a duration. The result of node_cpu_seconds_total[5m] is a range vector: for every series, the samples whose timestamp lies in [t - 5m, t]. The matrix is not a flat list of samples; each series keeps its own slice.

The two shapes, as the engine returns them:

Instant vector at t=10:00:00 (one sample per series):

  series node-a:9100 cpu=0 mode=idle  -> 12345.67
  series node-a:9100 cpu=1 mode=idle  -> 12340.12
  series node-b:9100 cpu=0 mode=idle  -> 11800.55
  ...

Range vector at t=10:00:00 with [5m] (matrix per series):

  series node-a:9100 cpu=0 mode=idle:
    sample at 09:55:30 -> 12340.00
    sample at 09:56:00 -> 12341.00
    sample at 09:56:30 -> 12342.50
    ...
    sample at 10:00:00 -> 12345.67

The range vector is not a sequence of instants; it is a single value that the engine materialises once and hands to whatever function consumes it. rate(...), increase(...), irate(...), deriv(...), predict_linear(...), delta(...), idelta(...) and the *_over_time(...) family all consume range vectors and return instant vectors.

The duration suffix [5m] is only legal in two positions: after a metric selector (node_cpu_seconds_total[5m]) and after a subquery ((expr)[5m:30s]). Anywhere else the parser rejects it. The reason is structural: a duration selects a time window relative to a series selector, and only those two constructs provide a series selector.

Why a sysadmin cares

The wrong-vector-shape failure is the second most common PromQL error after “the selector matches no series”. It has two flavours:

  • A query that returns a range vector where a panel expects an instant vector. Symptom: panel reads “No data”; the expression browser shows the data.
  • A query that consumes a range vector directly in an arithmetic operator. Symptom: the parser rejects the query at evaluation time, e.g. node_cpu_seconds_total[5m] / 100 errors out with 1:32: parse error: ranges only allowed for vector selectors.

Both flavours share the same root cause: the operator who wrote the query did not stop to ask “what shape does this expression return?”. The lesson exists to make that question reflexive.

A second reason: storage and cost. Range vectors are materialised by the engine at evaluation time. A range query that asks for metric[1h] at step=15s materialises 240 samples per series for every series in the selector. A recording rule that pre-aggregates the same data into a new metric avoids the re-materialisation cost; Part XVI returns to this.

How it works

The selector layer of the evaluation pipeline produces the matrix. When the parser sees metric[5m], it attaches a duration node to the selector node. At evaluation time, the selector calls into the TSDB index for matching series, then into the chunk reader for samples whose timestamps fall inside the window. The chunk reader returns a slice of float64s and timestamps per series. The function or operator that consumes the range vector walks each series’ slice independently.

The chunk reader’s role is critical: a range vector at t=10:00 on a metric scraped every 15 s returns up to 21 samples per series (5 minutes plus one). The TSDB stores samples in compressed 1-byte-or-2-byte-per-sample blocks; decompression is the latency that dominates a long-window rate() call. There is no shortcut in the engine for “skip every other sample”; the function reads all of them.

The subquery form expr[5m:30s] is a range vector whose samples are themselves the result of evaluating expr at step 30s across the last 5m. Each sub-step is a full evaluation; subqueries multiply the cost.

How to configure it

There is no PromQL configuration that selects between instant and range vectors; the shape is determined by the expression. The relevant production configuration is the scrape interval and the chunk encoding, both of which influence how many samples a range vector of a given window contains:

# prometheus.yml
global:
  scrape_interval: 15s       # scrape cadence; matches most exporters
  scrape_timeout: 10s        # per-scrape upper bound; < scrape_interval

scrape_configs:
  - job_name: node
    static_configs:
      - targets: ['node-a:9100', 'node-b:9100']
    metric_relabel_configs:
      # Drop high-cardinality labels before the TSDB sees them.
      # A 5-minute window on 1k cores produces 21k samples per
      # range vector; the cost compounds across dashboards.
      - action: labeldrop
        regex: 'cpu_detail|stack_trace'

The scrape interval determines how dense the samples are. A metric[5m] window on a metric scraped every 15 s contains up to 21 samples; scraped every 60 s, the same window contains up to 6. Lower scrape intervals increase the per-range-vector sample count and therefore the engine’s CPU cost; higher intervals leave rate() with too few samples for a stable reading.

How to validate it

The HTTP API is the source of truth for the shape:

# Instant query: returns data.resultType = "vector"
curl -s --data-urlencode 'query=node_cpu_seconds_total{mode="idle"}' \
  http://localhost:9090/api/v1/query \
  | jq '{resultType: .data.resultType, rows: (.data.result | length)}'
{
  "resultType": "vector",
  "rows": 16
}
# Range selector at the API still returns "vector"
# (instant query cannot return a range vector).
curl -s --data-urlencode 'query=node_cpu_seconds_total{mode="idle"}[5m]' \
  http://localhost:9090/api/v1/query

The instant-query endpoint refuses to return a range vector. It returns:

{
  "status": "error",
  "errorType": "bad_data",
  "error": "1:41: parse error: expected instant vector in call to function ...

because node_cpu_seconds_total{mode="idle"}[5m] is a range vector and the endpoint asked for an instant result. To validate a range vector, use the range-query endpoint with a single step:

curl -s --data-urlencode 'query=node_cpu_seconds_total{mode="idle"}[5m]' \
  --data-urlencode 'time=2026-08-13T10:00:00Z' \
  http://localhost:9090/api/v1/query \
  | jq '{resultType: .data.resultType, samples: (.data.result[0].values | length)}'

The error you see when the expression is invalid:

{
  "status": "error",
  "errorType": "bad_data",
  "error": "1:39: parse error: ranges only allowed for vector selectors"
}

This message names the constraint directly: ranges are only allowed directly after a selector (or a subquery). Any other position is a syntax error.

The four checks for a “no data” panel:

  1. data.resultType is "vector". If it is "matrix", the panel cannot plot the result; rewrite the expression to call a range-consuming function.
  2. data.result is non-empty. Empty means the selector matched no series.
  3. For each series, the timestamp in value[0] is the evaluation time. If it is missing or stale, the scrape failed.
  4. The sample is a valid float. NaN, Inf and -Inf are valid and survive the JSON round-trip; plotting them depends on the panel.

How it can fail

The five failure modes specific to vector shapes:

  1. Range vector returned to a panel. A query like metric[5m] directly in a panel. The panel sees a resultType of "matrix" and renders nothing. The fix is to wrap the range vector in a function that returns an instant vector: sum_over_time(metric[5m]), rate(metric[5m]), count_over_time(metric[5m]).
  2. Range vector used as arithmetic operand. metric[5m] / 100 errors at parse time. Wrap the range vector in a function first, then operate.
  3. Range vector too short. rate(metric[30s]) on a metric scraped every 15 s produces a matrix with 2 or 3 samples. rate() requires enough samples for a stable linear regression; the standard guidance is to use [4 * scrape interval] or longer. With two samples the result is NaN.
  4. Empty range vector. A range vector with zero samples inside the window. The function returns nothing for that series. rate() on an empty range vector is NaN. The common cause is a metric whose series was just created (or just restarted) and has not yet accumulated history.
  5. Subquery explosion. rate(metric[5m])[1h:15s] evaluates rate(metric[5m]) 240 times per series. Each evaluation reads the last 5 minutes of samples from the TSDB. The cost multiplies; cap step and width carefully.

Security implications

The vector-shape rules do not directly create a security surface. The failure shape is a denial-of-service against the engine: a query with a subquery inside a subquery, e.g. rate(metric[5m])[1d:1m], evaluates 1440 times per series and will exceed --query.timeout quickly. The discipline is the same as any heavy query: pre-aggregate into a recording rule, then have the panel read the pre-aggregated series.

Performance implications

The cost of a range vector is proportional to (number of series) x (samples per series in the window). The samples per series is bounded by window / scrape_interval. Two levers:

  • Shorter window. metric[1m] on a 15 s scrape is up to 5 samples per series; metric[5m] is up to 21.
  • Aggregation before the range vector. sum without(cpu) (rate(metric[5m])) aggregates across CPUs first, reducing the number of series the window must span. The range vector in the outer expression reads far fewer slices.

The standard rate() window is [4 * scrape_interval], i.e. 1 minute on a 15 s scrape. Going shorter risks an unstable rate; going longer pays in CPU and memory.

Verification

You should now be able to answer:

  • What is the difference in shape between an instant vector and a range vector?
  • Why can [5m] appear after a metric selector but not after an arithmetic operator?
  • Which PromQL functions consume a range vector and return an instant vector?
  • What does the engine return when a query yields a range vector and the caller asked for an instant result?

Quiz

Knowledge check · 8 questions

  1. Q1. What is the result type of the bare selector node_cpu_seconds_total{mode=&#34;idle&#34;}?

  2. Q2. What is the result type of node_cpu_seconds_total[5m] when evaluated as an instant query?

  3. Q3. Which of these functions consume a range vector and return an instant vector?

  4. Q4. The duration suffix [5m] is legal after any expression in PromQL.

  5. Q5. Which PromQL error message tells you the duration suffix is in the wrong position?

  6. Q6. On a metric scraped every 15 s, how many samples does a [5m] range vector typically contain per series?

  7. Q7. What happens when a Grafana time-series panel is given a query whose resultType is matrix?

  8. Q8. Which conditions make rate(metric[5m]) return NaN for a series?

Passing score: 75%. Answers are checked in this browser.