ObservabilityXII · PromQL FoundationsPromQLFoundations
Instant and Range Vectors
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
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] / 100errors out with1: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:
data.resultTypeis"vector". If it is"matrix", the panel cannot plot the result; rewrite the expression to call a range-consuming function.data.resultis non-empty. Empty means the selector matched no series.- For each series, the timestamp in
value[0]is the evaluation time. If it is missing or stale, the scrape failed. - The sample is a valid float.
NaN,Infand-Infare 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:
- 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]). - Range vector used as arithmetic operand.
metric[5m] / 100errors at parse time. Wrap the range vector in a function first, then operate. - 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 isNaN. - 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 isNaN. The common cause is a metric whose series was just created (or just restarted) and has not yet accumulated history. - Subquery explosion.
rate(metric[5m])[1h:15s]evaluatesrate(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
Q1. What is the result type of the bare selector node_cpu_seconds_total{mode="idle"}?
Q2. What is the result type of node_cpu_seconds_total[5m] when evaluated as an instant query?
Q3. Which of these functions consume a range vector and return an instant vector?
Q4. The duration suffix [5m] is legal after any expression in PromQL.
Q5. Which PromQL error message tells you the duration suffix is in the wrong position?
Q6. On a metric scraped every 15 s, how many samples does a [5m] range vector typically contain per series?
Q7. What happens when a Grafana time-series panel is given a query whose resultType is matrix?
Q8. Which conditions make rate(metric[5m]) return NaN for a series?
Passing score: 75%. Answers are checked in this browser.