A slow Grafana dashboard backed by Prometheus often comes from panels that recalculate the same heavy PromQL expression on every refresh. A Prometheus recording rule stores that expression as a new time series, so the dashboard can read a ready-made metric instead of asking Prometheus to aggregate raw samples repeatedly.
Recording rules run inside Prometheus at the rule group interval and write their output back into the local time-series database. Grafana still uses the same Prometheus data source, but the panel query changes from a nested expression to the recorded metric name.
The recorded series starts only after the rule is loaded and evaluated; it does not backfill old dashboard history. Keep the labels that dashboard variables or legends need, validate the rule file before reload, and confirm the final panel through Query Inspector so the saved dashboard is using the new series.
sum without (instance, pod) (rate(http_requests_total{job="checkout-api"}[5m]))
Use Query Inspector → Stats before the change to note the request time and returned rows for the panel.
Related: How to use Grafana query inspector
Recorded metric: job:http_requests:rate5m Kept labels: job
Prometheus recording rule names commonly describe the aggregation level, source metric, and operation. A name such as job:http_requests:rate5m is easier to recognize in Grafana than a copied expression.
$ sudo vi /etc/prometheus/rules/dashboard.rules.yml
groups: - name: dashboard-rollups interval: 1m rules: - record: job:http_requests:rate5m expr: sum without (instance, pod) (rate(http_requests_total{job="checkout-api"}[5m]))
Keep only labels that the panel, legend, or template variables need. Keeping high-cardinality labels such as pod or instance can preserve the dashboard load that the rule is meant to remove.
$ sudo vi /etc/prometheus/prometheus.yml
rule_files: - /etc/prometheus/rules/dashboard.rules.yml
If the configuration already has a rule_files list, add the dashboard rule file to that list instead of creating a second top-level key.
$ promtool check rules /etc/prometheus/rules/dashboard.rules.yml Checking /etc/prometheus/rules/dashboard.rules.yml SUCCESS: 1 rules found
Prometheus applies rule-file changes only when the configured rule files are well formed.
Related: How to test Prometheus rule files
$ curl -i -X POST http://prometheus.example.net/-/reload HTTP/1.1 200 OK ##### snipped #####
The HTTP reload endpoint works only when Prometheus is started with --web.enable-lifecycle. If that endpoint is disabled, use the reload method already approved for the Prometheus service, such as sending SIGHUP through the service manager.
$ promtool query instant http://prometheus.example.net 'job:http_requests:rate5m'
job:http_requests:rate5m{job="checkout-api"} => 42.81 @[1781909952.22]
An empty result means the source expression had no samples for the selected range, the rule has not evaluated yet, or Prometheus did not load the rule file.
job:http_requests:rate5m{job="$job"}
Keep any dashboard variable matchers that still belong on the recorded series. Remove matchers for labels that the recording rule intentionally aggregated away.
A recorded metric begins at the reload time, so older time ranges can still show No data until enough rule evaluations exist.
Stats should show returned rows for the panel, and Query should show job:http_requests:rate5m in the request payload. Compare the request time with the baseline from the original expression before removing the old query from notes or runbooks.
Related: How to use Grafana query inspector