ObservabilityXXVIII · Grafana VariablesGrafanaVariables
Template Variable Purpose
What you'll learn
- Define a template variable in Grafana 11 and explain what operational problem it solves at runtime
- Read the var-state that Grafana encodes into the dashboard URL and use it to deep-link, share, and bookmark a filtered view
- Trace the dependency chain between variables (cluster to namespace to service) and recognise when a chain order is wrong
- Distinguish curated (dashboard-level) variables from ad-hoc (panel-scoped) variables and choose the right one for each use
- Recognise the failure shapes that surface only when variables interact with multi-value selections and the time-range picker
Prerequisites
- 01-panels-and-queries
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 production incident lands at 02:40. The on-call engineer opens
the cluster overview dashboard, glances at the wrong panel, and
pages the wrong team. The post-incident review asks the obvious
question: why does a dashboard that is meant to show the cluster
only show one of the twelve running? The answer is that someone
edited the URL the day before and forgot. The variable dropdown
says eu-west-1; the URL says var-cluster=eu-west-1; every
panel is bound to the variable; the page is technically correct
and operationally misleading.
This lesson is about the thing at the top of every reusable dashboard: the template variable. A template variable is a named, label-driven, runtime parameter. It looks like a dropdown. It behaves like a query parameter. It is encoded into the URL. It is shared by every panel on the dashboard.
What it is
A template variable in Grafana 11 is a dashboard-scoped, named parameter whose value is selected by the viewer at view time and substituted into panel queries, panel titles, and panel descriptions. The substitution is performed by the Grafana server before the query is sent to the data source.
A dashboard can declare any number of template variables. Each one has a name, a type (query, custom, constant, datasource, textbox, interval, or system), a definition, a refresh policy (on dashboard load, on time-range change, never), a current value (which the viewer can override), and an optional list of display and hide controls.
The simplest model is: a variable is a labelled query that becomes a dropdown, and the dropdown changes the panel query at view time.
Why a sysadmin cares
Five operational pains map directly to the decision “use a variable” rather than “duplicate the dashboard”:
- One dashboard, N views. A cluster overview that shows
eu-west-1for the EU engineer andus-east-2for the US engineer is one URL away from being two views. Without variables, the answer is N dashboards that drift apart. - Bookmarkable, shareable state. The URL encodes every variable value plus the time range. A Slack link to a specific panel of a specific service at a specific time is a single URL. Without variables, the recipient must repeat the filter.
- Consistent filtering across panels. A service-name variable on the dashboard means every panel filters on that service name. A copy-paste mistake on a single panel is impossible.
- Driven by the data, not by the author. A query-type
variable reads
label_values(up, cluster)from Prometheus every dashboard load. New clusters appear in the dropdown the moment they are scraped. The dashboard author does not edit a list. - Cheap to fix a typo. The same variable name is used everywhere. Renaming the metric or the label is a single edit at the dashboard root.
The cost is paid in query blast and variable-query latency (covered in the lesson on query cost) and in the discipline of keeping variable chains short.
How it works
The lifecycle of a variable on dashboard load runs through six stages:
dashboard load
|
v
+-------------------+ +-------------------+
| resolve chain |-->| for each var, |
| order by | | run the variable |
| dependencies | | query against |
| | | the bound data |
+-------------------+ | source |
+-------------------+
|
v
+-------------------+
| merge values |
| with the |
| "all" / "any" |
| pseudo-value |
+-------------------+
|
v
+-------------------+
| build the URL |
| ?var-x=a |
| &var-y=b |
| &from=... |
| &to=... |
+-------------------+
|
v
+-------------------+
| every panel |
| query is re-run |
| with $x and $y |
| substituted |
+-------------------+
|
v
pixels
Two facts about the model that shape the design:
- Variables are resolved before panels run. A variable query
is one of the first things Grafana does on load. If the
variable query is slow, the dashboard is slow. The data
sources backing variable queries must be fast; an
__metricsPrometheus is the usual answer for production. - The URL is the source of truth for “current values”. A viewer can land on a dashboard from any link; the variables in the URL are what the panels bind to. Selecting a value from a dropdown writes the URL; refreshing the page re-reads it.
The chaining pattern
A variable can reference another variable. The pattern looks like:
$cluster (no dependency; the root)
|
v
$namespace (depends on $cluster)
|
v
$service (depends on $namespace)
|
v
$instance (depends on $service)
Each variable’s query filters on the previous variable’s
current value. The dependency is declared by writing
$cluster inside the $namespace query. The chain is
topological; a cycle depends on the order Grafana loads them
in and breaks silently.
A correct chain means the dropdown narrows as the operator
drills down. A wrong chain (say $namespace before $cluster)
means the namespace list is unfiltered, the dropdown is
unbounded, and the dashboard takes minutes to load.
Ad-hoc versus curated variables
Grafana 11 distinguishes two scopes:
+------------------------+ +------------------------+
| Curated variable | | Ad-hoc variable |
| (dashboard-level) | | (panel-scoped) |
+------------------------+ +------------------------+
| Declared in | | Declared at query |
| Settings > Variables | | time via the |
| | | variable edit menu |
| Visible to every | | Visible only in the |
| panel on dashboard | | panel that uses it |
| | | |
| URL-encoded with | | NOT URL-encoded; |
| var-name=value | | ephemeral |
| | | |
| Refreshing chain | | Re-runs on every |
| cost is amortised | | panel refresh; |
| across panels | | cost is per-panel |
+------------------------+ +------------------------+
The default and the production default is curated. Ad-hoc variables are a panel-edit convenience that turn into operational debt the moment another team member opens the dashboard. Promote an ad-hoc variable to a curated one the moment it is reused across panels.
How to configure it
A production-shaped templating.list for a Kubernetes
service-overview dashboard looks like:
{
"templating": {
"list": [
{
"name": "cluster",
"type": "query",
"datasource": { "type": "prometheus", "uid": "prom-prod" },
"query": "label_values(up{job=\"kube-state\"}, cluster)",
"refresh": 1,
"includeAll": true,
"multi": true,
"allValue": ".*",
"sort": 1,
"current": { "selected": true, "text": "All", "value": "$__all" }
},
{
"name": "namespace",
"type": "query",
"datasource": { "type": "prometheus", "uid": "prom-prod" },
"query": "label_values(up{job=\"kube-state\",cluster=~\"$cluster\"}, namespace)",
"refresh": 1,
"includeAll": true,
"multi": true,
"allValue": ".*",
"sort": 1,
"current": { "selected": true, "text": "All", "value": "$__all" }
},
{
"name": "service",
"type": "query",
"datasource": { "type": "prometheus", "uid": "prom-prod" },
"query": "label_values(up{job=\"kube-state\",cluster=~\"$cluster\",namespace=~\"$namespace\"}, service)",
"refresh": 1,
"includeAll": true,
"multi": true,
"allValue": ".*",
"sort": 1,
"current": { "selected": true, "text": "All", "value": "$__all" }
}
]
}
}
The three variables form a chain: cluster is the root,
namespace references cluster, service references both.
Three details are production defaults:
refresh: 1— refresh on dashboard load. Cost is amortised across every panel that reads the variable.includeAll: trueplusallValue: ".*"— the All selection expands to a regex.*that bypasses filtering. This is how the dropdown says “do not filter on this dimension”.multi: true— selecting multiple clusters adds them to the URL as a comma-separated value and the regex=~matches them.
How to validate it
Three checks confirm the chain is wired correctly.
Severity: READ-ONLY.
# 1. The dashboard JSON has the variables in the order
# the dependency graph expects. Use jq to dump and read.
curl -s -u admin:$ADMIN \
https://grafana.example.com/api/dashboards/uid/svc-overview \
| jq '.dashboard.templating.list[] | {name, refresh, query}'
{
"name": "cluster",
"refresh": 1,
"query": "label_values(up{job=\"kube-state\"}, cluster)"
}
{
"name": "namespace",
"refresh": 1,
"query": "label_values(up{job=\"kube-state\",cluster=~\"$cluster\"}, namespace)"
}
{
"name": "service",
"refresh": 1,
"query": "label_values(up{job=\"kube-state\",cluster=~\"$cluster\",namespace=~\"$namespace\"}, service)"
}
# 2. Each variable's query runs in Prometheus and returns
# the expected value set. The variable query should
# never return empty in production.
curl -G -s http://prometheus:9090/api/v1/query \
--data-urlencode 'query=label_values(up{job="kube-state"}, cluster)' \
--data-urlencode 'time='$(date +%s) | jq '.data.result[].metric'
{
"cluster": "eu-west-1"
}
{
"cluster": "eu-west-2"
}
{
"cluster": "us-east-1"
}
{
"cluster": "us-east-2"
}
# 3. The URL encodes the chain. Open the dashboard with one
# cluster pinned, copy it, and confirm var-cluster is in
# the URL string.
xdg-open "https://grafana.example.com/d/svc-overview?var-cluster=eu-west-1"
The URL is the source of truth for the next person who opens the dashboard. If a value is missing from the URL, the dashboard defaults to All.
How it can fail
Five failure shapes appear repeatedly in dashboards that rely on variables:
- Stale variable list. The variable query returns
label_values(up, cluster)andupis being scraped, but the new cluster has not yet scraped. The variable dropdown does not show the new cluster. Symptom: an operator creates a new cluster, opens the dashboard, and the cluster is missing. - Unbounded variable query. A
queryvariable that selects onlabel_values(... , cluster)without a metric selector that scopes to known scrape targets returns every label value Prometheus has ever seen, including relabeling artefacts. The dropdown becomes unscrollable. Symptom: the dashboard takes seconds to open and the dropdown is huge. - Cycle in the dependency graph.
$areferences$band$breferences$a. Grafana logsTemplatingSrv: circular dependency detectedand one of the two variables ends up empty. Symptom: a blank dropdown with no error in the panel. - Drifted URL state. A viewer bookmarks a dashboard
with
var-cluster=eu-west-1; the cluster is later renamed toeu-west-1a; the URL still pins the old name; the dropdown shows the new name but every panel query uses the old name and returns nothing. - Multi-value without
=~. A variable is set tomulti: trueand the panel query usescluster="$cluster", which only matches a single value. When the viewer selects two clusters, the panel goes blank. Symptom: selecting the second cluster blanks a panel that was working with one cluster.
How to troubleshoot it
The diagnostic order is the same every time:
- Inspect the URL. Look at the
var-name=valuepairs. If the value is wrong, the panel is right to show wrong data. - Open the variable editor. Settings > Variables. For each variable, click Run query (the refresh icon at the bottom of the preview list). A failed variable query is the dashboard’s load-time problem.
- Run the variable query directly against the data
source. Take the JSON
queryfield, paste it into the data source’s own query interface (Prometheus’s graph page, Loki’s LogQL), and confirm the value list. If Prometheus has the data, the variable definition is the bug. - Inspect the chain order. Topological-sort the
queryfields by\$references. The chain must run from broadest to narrowest. - Disable the variable temporarily. Set the variable
to
hide: variableandtype: customwith a single value. The dashboard renders without the chain. If it renders correctly, the variable is the bug. - Look at the server logs.
grafana-serverlogs the variable resolution at debug level. Filter forTemplatingSrvto see the per-variable round-trip time.
Security implications
- Variables are URL-controlled. Anyone with the URL can pin a variable to any value the variable definition permits. A reader with view-only access cannot pin a value the variable definition has not offered; a viewer with edit access can replace the variable definition entirely. Treat dashboard edit access as equivalent to data-source read access.
- The variable query runs against the data source with the data source’s permissions. A variable that queries Loki queries it as the data-source plugin credential, not as the viewer. This is normally fine but means a viewer can drive a Loki query that the viewer would not be allowed to author in Explore.
allValueis a regex. Aqueryvariable withallValue: ".*"andmulti: truelets the viewer match every value the data source has. Against Prometheus, this is bounded by what the data source itself exposes; against a non-paged query, this can be unbounded.
Performance implications
- The dashboard load is dominated by variable queries, not
by panel queries. Each variable is one round trip to the
data source; with
refresh: 2(on time-range change) it re-runs on every time-range change. - A chain of three variables is three round trips. The
total load is
t_chain + t_panel. The chain is the silent cost. includeAllandmultiare not free on the data source side. A panel withcluster=~"$cluster"where$clusteriseu-west-1|eu-west-2is a multi-match query. The panel is bounded by the regex and the matcher; a value set of 1,000 cluster names is a thousand-match regex the data source has to evaluate.- Long chains are expensive. A chain of seven variables,
each with
refresh: 1, is seven calls before a single panel runs.
Production guidance
- Keep variable chains short. Three variables is a reasonable upper bound; five is a smell.
- Default to
curated(dashboard-level) variables. Promote any ad-hoc variable that more than one panel uses. - Use
multi: trueandincludeAll: truefor filtering dimensions the viewer usually wants to expand. Usemulti: falsefor dimensions where picking multiple has no operational meaning. - Document the chain order in the dashboard description
(
Settings > Description). “Top to bottom: cluster narrows to namespace narrows to service.” - Verify the URL after every variable rename. A bookmarked
URL with a stale
var-nameis a hidden lie.
Verification
You should now be able to answer:
- What does a template variable do at view time?
- Where in the dashboard load lifecycle do variable queries run?
- How is the URL related to the variable state?
- What is the difference between a curated and an ad-hoc variable, and which should a production dashboard use?
- Why does the chain order matter?
Quiz
Knowledge check · 8 questions
Q1. What problem does a template variable solve at view time?
Q2. Where does Grafana 11 store the current value of a curated template variable?
Q3. A variable chain must be ordered from narrowest dimension to broadest for the dashboard to load quickly.
Q4. Which kind of variable should a production dashboard prefer?
Q5. Name the dashboard URL parameter that pins the value of a curated variable called cluster.
Q6. Which of these are properties of a curated template variable?
Q7. A dashboard with five variables of ten values each and three panels is roughly how many backend requests per refresh?
Q8. A colleague shares a Slack link to a dashboard pinned to var-cluster=eu-west-1. What does the link carry?
Passing score: 75%. Answers are checked in this browser.