Prometheus recording rules: pre-aggregating expensive queries
Published: 2026-03-11
Prometheus range queries over high-cardinality labels are slow. A 7-day request rate across hundreds of pods computed on every Grafana panel load adds up. Recording rules solve this by pre-computing the result at scrape time.
How recording rules work
A recording rule is a PromQL expression that runs on an interval and writes the result as a new metric with a simpler name. Grafana queries the pre-computed metric instead of the expensive original expression.
yamlapiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: recording-rules
namespace: observability
labels:
release: kube-prom-stack # must match Prometheus selector
spec:
groups:
- name: recording
interval: 30s
rules:
- record: job:http_requests_total:rate5m
expr: |
sum by (job, namespace) (
rate(http_requests_total[5m])
)
interval: 30s means the result is updated every 30 seconds. Grafana panels using job:http_requests_total:rate5m respond instantly — no heavy range query at load time.
Naming convention
The standard: level:metric:operations
level— the aggregation level (job,namespace,cluster,instance)metric— the source metric nameoperations— what was done (rate5m,sum,p99,ratio)
Examples:
namespace:container_cpu_usage_seconds:rate5m
job:http_request_duration_seconds:p99
cluster:node_memory_working_set_bytes:sum
job:http_errors:rate5m_ratio
Practical rules for a multi-cluster setup
yamlgroups:
- name: cluster-resources
interval: 60s
rules:
# CPU usage per namespace — for quota dashboards
- record: namespace:container_cpu_usage_seconds_total:rate5m
expr: |
sum by (namespace, cluster) (
rate(container_cpu_usage_seconds_total{container!=""}[5m])
)
# Memory working set per pod — for pod detail panels
- record: pod:container_memory_working_set_bytes:sum
expr: |
sum by (pod, namespace, cluster) (
container_memory_working_set_bytes{container!=""}
)
# HTTP error rate per job
- record: job:http_errors:rate5m
expr: |
sum by (job, namespace) (
rate(http_requests_total{status=~"5.."}[5m])
)
/
sum by (job, namespace) (
rate(http_requests_total[5m])
)
# P99 latency per job
- record: job:http_request_duration_seconds:p99
expr: |
histogram_quantile(0.99,
sum by (job, namespace, le) (
rate(http_request_duration_seconds_bucket[5m])
)
)
SLO burn rate
Alerting on SLO burn rate requires range queries over multiple windows. Pre-compute them:
yaml - name: slo-burn
interval: 60s
rules:
- record: job:slo_error_budget_burn:1h
expr: |
(
sum by (job) (rate(http_requests_total{status=~"5.."}[1h]))
/
sum by (job) (rate(http_requests_total[1h]))
) / 0.001 # error budget = 0.1% = 0.001
- record: job:slo_error_budget_burn:6h
expr: |
(
sum by (job) (rate(http_requests_total{status=~"5.."}[6h]))
/
sum by (job) (rate(http_requests_total[6h]))
) / 0.001
- record: job:slo_error_budget_burn:24h
expr: |
(
sum by (job) (rate(http_requests_total{status=~"5.."}[24h]))
/
sum by (job) (rate(http_requests_total[24h]))
) / 0.001
Multi-window alert using these recordings fires faster and with less false-positive noise:
yaml- alert: HighErrorBudgetBurn
expr: |
job:slo_error_budget_burn:1h > 14
and
job:slo_error_budget_burn:6h > 6
for: 2m
labels:
severity: critical
annotations:
description: "{{ $labels.job }} is burning error budget at {{ $value | humanize }}x rate"
The 14x burn rate at 1h means you'll exhaust the monthly error budget in ~2 days at this rate.
Kubernetes resource rules
These are the most commonly needed:
yaml- name: k8s-resource-recording
interval: 60s
rules:
# Node CPU utilization
- record: node:cpu_utilization:ratio
expr: |
1 - avg by (node) (
rate(node_cpu_seconds_total{mode="idle"}[5m])
)
# Node memory pressure
- record: node:memory_utilization:ratio
expr: |
1 - (
node_memory_MemAvailable_bytes /
node_memory_MemTotal_bytes
)
# PVC usage
- record: persistentvolumeclaim:disk_usage:ratio
expr: |
1 - (
kubelet_volume_stats_available_bytes /
kubelet_volume_stats_capacity_bytes
)
Where to put PrometheusRule objects
Recording rules go in base/monitoring/rules/recording.yaml and are applied on every cluster by the base Kustomization. Per-environment rules (different SLO thresholds for dev vs prod) go in patches/monitoring/rules/.
One PrometheusRule per concern: recording, node alerts, SLO alerts. Keeps the diff small when adjusting thresholds.
Verifying rules are loaded
bashkubectl port-forward -n observability svc/kube-prom-stack-prometheus 9090:9090
# http://localhost:9090/rules — shows all loaded recording and alerting rules
# Check if a recording rule is producing data
curl -s 'http://localhost:9090/api/v1/query?query=job:http_requests_total:rate5m' | jq '.data.result | length'
If the rule isn't in the list, check the PrometheusRule label selector matches the kube-prom-stack configuration.