Skip to main content
RunBook Academy

ObservabilityXVII · Recording RulesRecordingRules

Recording Rule Purpose

Intermediate⏱ ~18 minbash

What you'll learn

  • Explain what a recording rule precomputes and where the new series is stored
  • Identify the three patterns where recording rules pay off: rate-of-rate, top-k, large join
  • Distinguish a recording rule from an alerting rule and place each in the right role
  • Locate rule files in the Prometheus configuration and validate them with promtool and the rules API

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.

At 03:14 an on-call engineer opens the latency dashboard. Every panel that touches p99 hangs for twelve seconds. Prometheus is at 99 percent CPU. The dashboard uses histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m]))) on every panel load, across forty instances, in six clusters. The query has no record. Every dashboard refresh re-runs it. The cluster runs four dashboards with the same panel; Alertmanager also evaluates the same expression every thirty seconds. The cost is paid over and over.

This is the operational shape that recording rules exist to prevent. A recording rule stores the result of a PromQL expression as a new time series. The expression is evaluated at a fixed interval. The result is written to the local TSDB under the name given by record:. Dashboards, alerts, and other rules read the cached series instead of recomputing the expression on every query.

A team without recording rules pays the query cost on every alert evaluation and every dashboard load. A team with the right recording rules pays a constant cost per metric per evaluation interval. The difference is operational.

What it is

A recording rule is a YAML-stored PromQL expression with a name. The Prometheus server evaluates the expression at a fixed interval (configurable per group, default evaluation_interval from the global section of prometheus.yml). The result is written to the local TSDB as one or more new series. The new series is queryable immediately after evaluation completes. From the query engine’s perspective, the rule output is just another time series: it obeys the same retention rules, the same label semantics, and the same PromQL grammar as any raw metric.

The shape of a rule is:

groups:
  - name: <group_name>
    interval: 30s          # optional, defaults to global evaluation_interval
    rules:
      - record: <new_metric_name>
        expr: <PromQL expression>
        labels:             # optional, static labels added to every output series
          team: payments

Three pieces matter:

  • record: is the name the new metric is stored under. The convention is level:metric_name:operations (lesson 02 covers the convention in depth).
  • expr: is the PromQL expression to evaluate. It can reference raw metrics and other recording rules.
  • labels: is an optional map of static labels applied to every output series. Useful for ownership (team, service, tier).

Recording rules and alerting rules share the same YAML format and the same evaluation engine. The difference is the output:

  • Recording rule produces a new time series. Consumers are dashboards, alerts, and other rules.
  • Alerting rule produces a pending or firing alert. Consumers are Alertmanager and the on-call rotation.

A team that uses recording rules for SLO inputs and alerting rules for SLO breaches gets the right division of labour. The recording rule produces the SLI. The alerting rule produces the page. The two are coupled through the metric name.

Why a sysadmin cares

Three operational pain points disappear when the right recording rules are in place.

  1. Dashboard load time becomes independent of the query cost. A panel that reads from a recording rule evaluates a single metric lookup. A panel that recomputes histogram_quantile() per render pays the cost every time the dashboard loads.
  2. Alert evaluation cost stays bounded. An alert that reads job:http_requests:rate5m evaluates against a precomputed series. An alert that computes the rate itself pays the rate cost per evaluation; if there are ten copies of the same alert on different teams, the cost multiplies.
  3. SLO computations become stable. The SLI is computed once per minute and reused across dashboards and alerts. Both consumers read the same number; the alert cannot disagree with the dashboard because they read the same source.

The trade-off is also real: a recording rule costs CPU every evaluation interval regardless of whether the result is consumed. A rule that feeds one dashboard loaded once per hour by one user is wasted work. The lesson is to record what is computed many times, not what is computed once.

How it works

The flow is the same as a regular PromQL query, but the result is persisted.

   Raw metric (e.g. http_requests_total)
       |
       |  PromQL expression evaluated at the group's interval
       v
   Recording rule output
   (e.g. job:http_requests:rate5m)
       |
       +--- Dashboard panel A
       +--- Dashboard panel B
       +--- Alerting rule
       +--- Other recording rules

The evaluator is a goroutine per group, started by the rule manager in rules/manager.go. Each goroutine ticks at the group’s interval, evaluates its rules in declaration order, and writes the results to the local TSDB. The goroutine is independent of other groups; a slow group does not block a fast group.

When recording rules pay off

Three patterns appear repeatedly in production rule sets.

  1. Rate-of-rate. A five-minute rate of a one-minute rate is expensive. The outer rate requires the inner rate to be evaluated first. A recording rule stores the inner rate; the outer expression becomes a single rate call against a precomputed series.
  2. Top-k on a wide input. topk(10, sum by (job, instance) (rate(...))) requires a fan-out over every label set on every query. A rule precomputes the per-(job, instance) rate; the top-k is a cheap selection against the precomputed series.
  3. A large join. A recording rule can hold the join key stable (a label derived from another metric, a topology lookup). The downstream panel reads the join result without recomputing it. The rule is the cache.

When a rule does not pay off:

  • The expression is trivial and evaluated by one panel. The rule adds overhead with no benefit.
  • The expression is computed once per dashboard load by a panel nobody opens. Same reasoning.
  • The rule output is consumed by a single alert that fires infrequently. Recomputing per alert evaluation is cheap.

How to configure it

A typical recording-rules file in /etc/prometheus/rules/api.recording.yml:

# Per-job request rate. Consumed by:
#   - dashboard latency-overview
#   - alert HighRequestRate
#   - SLO rule job:api:request_rate (lesson 06)
groups:
  - name: api-recording
    interval: 30s
    rules:
      - record: job:http_requests_total:rate5m
        expr: sum by (job) (rate(http_requests_total[5m]))

      - record: job:http_requests_total:irate1m
        expr: sum by (job) (irate(http_requests_total[1m]))

      - record: job:http_request_duration_seconds:p99
        expr: |
          histogram_quantile(
            0.99,
            sum by (job, le) (
              rate(http_request_duration_seconds_bucket[5m])
            )
          )

Three things to notice:

  • Group interval: 30s. Faster than the global default of one minute. The cost is doubled per evaluation; the benefit is fresher data for the alerts that consume it.
  • sum by (job). The level is job. The output series have one label: job. Lesson 02 covers why the level is in the metric name.
  • histogram_quantile() over sum by (job, le). The le label is mandatory for the inner aggregation; lesson 02-histogram-quantile covers the failure mode in depth.

The prometheus.yml reference:

rule_files:
  - /etc/prometheus/rules/*.yml

global:
  evaluation_interval: 1m

evaluation_interval is the default for any group that does not override interval. A group that sets interval: 30s runs twice as often; a group that sets interval: 5m runs five times less often. Use per-group intervals to match the SLO or alert sensitivity the rule feeds.

How to validate it

Three checks, each catching a different failure shape.

1. promtool validates the YAML and the PromQL.

promtool check rules /etc/prometheus/rules/api.recording.yml

Expected output:

SUCCESS: rules are valid

A non-zero exit means the file has either a YAML syntax error or a PromQL expression that does not parse. promtool reports the file and line.

2. The rule is loaded by Prometheus.

curl -s http://localhost:9090/api/v1/rules \
  | jq '.data.groups[] | select(.name=="api-recording") | .rules[] | {name, health, lastError, evaluationTime}'

Expected output (illustrative):

{
  "name": "job:http_requests_total:rate5m",
  "health": "ok",
  "lastError": "",
  "evaluationTime": 0.0123
}

health: ok and an empty lastError are the green light. health: err and a populated lastError mean the rule loaded but fails to evaluate.

3. The rule produced series.

curl -s 'http://localhost:9090/api/v1/query?query=job:http_requests_total:rate5m' \
  | jq '.data.result | length'

Expected: a positive integer (one per job label set). Zero means the rule is loaded and evaluating but producing nothing — usually a wrong source metric, a typo in expr:, or an aggregation that drops every series.

How it can fail

Six failure modes, in the order they appear in real incidents.

  1. Missing source metric. A rule references http_requests_total but the exporter was removed or renamed. The rule produces no series for that evaluation. The dashboard panel reads “no data”. The alert reads “no data” and does not fire. The platform logs an evaluation failure; nobody watches the log.
  2. Wrong aggregation level. A rule’s expression uses sum by (job, instance) (...) but the rule’s name says job:.... The output series carry an instance label that consumers do not expect. Dashboards that grouped by job now show one panel per instance — a cardinality explosion that looks like a metrics outage.
  3. Recursive rule without backstop. Rule A depends on rule B; rule B depends on rule A. Prometheus does not detect cycles; one of the rules reads the previous tick’s value and the result lags by one interval. The lag is silent.
  4. Label churn. The source metric carries a label whose cardinality is unbounded (e.g. email, request_id, trace_id). The rule passes the label through. The TSDB series count grows without bound. OOM in days.
  5. Rule interval shorter than the rule can complete. A rule takes ninety seconds to evaluate. The group’s interval is thirty seconds. Prometheus logs rule_group_iterations_missed_total > 0. The rule never catches up; downstream panels read stale data.
  6. Rule file fails to load. A YAML syntax error or invalid PromQL in one file prevents all rules in that file from loading. Other files continue. The team sees a startup message that looks fatal but is in fact scoped. Rules in the broken file never produce data; alerts they were meant to feed never fire.

How to troubleshoot it

When a dashboard reads “no data” or a recording rule’s output disagrees with what the same expression returns inline, the diagnosis order matters.

  1. Is the rule loaded? curl -s http://localhost:9090/api/v1/rules | jq '.data.groups[].file' — the rule’s file should appear in the list.
  2. Is the rule evaluating successfully? Same endpoint, drill into the rule: .data.groups[].rules[] | {name, health, lastError, evaluationTime}. health: ok and empty lastError are required.
  3. Is the rule producing series? curl -s 'http://localhost:9090/api/v1/query?query=RECORD_NAME' — a positive result count. Zero series means the expression returns nothing against the live TSDB.
  4. Is the source metric present? Evaluate the same expr: in the expression browser. If the raw expression returns data and the rule does not, the difference is in the record: name or the group’s interval (lesson 03 covers interval interactions).
  5. Is the rule’s interval too short for its workload? Check prometheus_rule_group_iterations_missed_total for the group. A non-zero value means the group missed at least one tick.
  6. Has cardinality exploded? Check prometheus_tsdb_head_series. A sudden rise after a rule change points to a label passed through by mistake.

Security implications

Rule files are loaded from the local filesystem. There is no network authentication on the rule parser itself. Three exposures matter.

  1. /api/v1/rules exposes rule content to anyone with HTTP access to the Prometheus HTTP server. Production deployments restrict the API surface with --web.listen-address, network ACLs, or a reverse proxy with authentication. The /api/v1/rules endpoint returns the rule expressions, including any hardcoded secrets or internal hostnames that the rules reference.
  2. Rule output inherits the source metric’s labels. A rule with a typo that pulls a sensitive label (e.g. customer_email) into a new metric exposes the label on the /api/v1/query endpoint and to every dashboard. Treat rule output as carefully as the source metric.
  3. Recording rules do not introduce a new write attack surface. They write to the local TSDB with the same permissions as a scrape. The risk is the same as for any other metric ingestion path.

The platform-security part of the course covers Prometheus HTTP auth, network isolation, and the labelling guide.

Performance implications

A recording rule’s cost is paid every evaluation interval, regardless of consumption.

  • CPU: proportional to the number of series the expression produces. A rule that reads rate() over ten thousand series costs more per evaluation than one that reads a hundred.
  • TSDB write: proportional to the number of output series. Each output series is one sample per evaluation.
  • TSDB storage: proportional to the number of output series times the retention. A rule with ten thousand output series evaluated every thirty seconds over thirty days of retention produces roughly 10,000 * 2 * 86,400 * 30 = 51.8 billion samples. The TSDB compresses them; wall-clock storage is in the tens of gigabytes.

The trade-off is constant evaluation cost against repeated query cost. The rule wins when the expression is computed more than a handful of times per minute. The rule loses when it is computed once per dashboard load — the rule adds overhead with no benefit.

A useful rule of thumb: a rule that takes more than ten percent of its group’s interval to evaluate is a candidate for splitting into a separate group with a longer interval (lesson 03 covers group mechanics).

Production guidance

  • Use the level:metric:operations naming convention so consumers can infer the aggregation level from the metric name. Lesson 02 covers the convention in depth.
  • Prefer sum by (...) to sum without (...). The by clause is explicit about what is kept. without hides the intent and breaks when the source metric grows new labels.
  • Document the rule’s inputs, outputs, and consumers in a header comment. A rule named job:api:requests:rate5m with the comment “consumed by alert HighRequestRate and dashboard latency-overview” is self-describing.
  • Validate with promtool check rules before every reload. CI runs promtool on every pull request that touches a rule file.
  • Add the rule’s group and interval to the team’s runbook. The interval is part of the contract between the rule and its consumers; changing it without telling consumers is a silent behaviour change.
  • Audit the rule set quarterly. Sort rules by prometheus_rule_evaluation_duration_seconds_sum divided by the count to get the mean per-rule duration. The top decile is where the cost lives.

Verification

You should now be able to answer:

  • What does a recording rule precompute, and where is the result stored?
  • What three patterns make a recording rule pay off?
  • What is the difference between a recording rule and an alerting rule, and where does each belong in an SLO pipeline?
  • Where do recording rules live in the Prometheus configuration, and how do you confirm a rule is loaded and producing data?

Quiz

Knowledge check · 8 questions

  1. Q1. What does a recording rule produce when it evaluates?

  2. Q2. Which of the following is the strongest signal that a recording rule should be introduced?

  3. Q3. A recording rule and an alerting rule share the same YAML format and evaluation engine but produce different outputs.

  4. Q4. A rule named `job:http_requests:rate5m` evaluates `sum by (job, instance) (rate(http_requests_total[5m]))`. What is the operational problem?

  5. Q5. Which promtool subcommand validates a recording-rules file before reload?

  6. Q6. Which of the following are correct ways to confirm a recording rule is producing data? (Select all that apply.)

  7. Q7. A source metric that the rule depends on is removed. What is the most likely operational outcome?

  8. Q8. Which metric exposes the count of times a rule group missed its evaluation tick because the previous evaluation took longer than the group interval?

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