ObservabilityV · Prometheus ArchitecturePromArchitecture
Rules and the Alerting Pipeline
What you'll learn
- Organise recording and alerting rules into groups with deliberate evaluation intervals
- Trace an alert from expression through pending and firing to Alertmanager, including for and keep_firing_for
- Verify evaluation health with ALERTS, ALERTS_FOR_STATE and the rule-group metrics
- Diagnose failed evaluations, missed iterations and notifications that never reach Alertmanager
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
The page arrives at 03:12: “disk will fill within four hours on db-02”. Between “a metric crossed a line” and “a phone rang” there is an entire pipeline: rule files, an evaluation loop, a small state machine, and a notification queue into Alertmanager. Each stage fails differently, and “no alerts” is ambiguous until you know which stage is silent. This lesson is the map.
How rules are loaded
prometheus.yml points at rule files with rule_files (globs are
fine). Each file contains groups, and each group is evaluated
on its own clock:
groups:
- name: node-health
interval: 30s # default: global evaluation_interval (1m)
rules:
- record: job:node_cpu_utilisation:ratio_rate5m
expr: 1 - avg by (job, instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]))
- alert: NodeDown
expr: up{job="node"} == 0
for: 5m
keep_firing_for: 10m
labels:
severity: critical
annotations:
summary: 'Node {{ $labels.instance }} unreachable for 5m'
Groups run concurrently; the rules inside a group run sequentially, in file order, all stamped with the same evaluation timestamp. That ordering is what lets a recording rule feed a later rule inside one group cleanly.
Recording rules vs alerting rules
A recording rule (record:) precomputes a PromQL expression
and stores the result as a brand-new series, written at evaluation
time. It exists for speed and sanity: the expensive fleet-wide
join runs once per interval, not once per dashboard panel per
viewer per refresh.
An alerting rule (alert:) evaluates an expression and feeds
the result into the alert state machine. Labels become the alert’s
identity (severity is the routing key by convention);
annotations become the human-readable payload.
The alert state machine
expression true for satisfied
inactive ----------------> pending ----------------> firing
^ | |
| expression false | expression false | expression false
+-----------------------------+ | (keep_firing_for
| (timer resets) +--------------------------+ delays resolution)
+--------------------------------------------------------+
- pending: the expression is true, but
forhas not yet been satisfied. Any evaluation where the expression is false resets the timer to zero.for: 0fires on the first true evaluation. - firing: the expression has held continuously for
for. Only firing alerts are sent to Alertmanager. - keep_firing_for (default 0): after the condition clears, the alert stays firing for this long before resolving. It exists to dampen flapping page-worthy alerts; use it deliberately.
Two synthetic series track live alerts: ALERTS\{alertname, alertstate="pending"|"firing"\} is 1 while an alert is active, and
ALERTS_FOR_STATE carries the Unix timestamp at which the alert
went active. The latter is how a restarted Prometheus restores
in-progress for timers — as long as the series is still inside
the five-minute lookback window.
The notify pipeline
Firing alerts are POSTed to every configured Alertmanager
(/api/v2/alerts), continuously, for as long as they fire. Two
config surfaces shape what leaves the building:
alert_relabel_configsrewrites or drops alerts before sending — the standard way to keepseverity: infooff the pager entirely.external_labelsare attached to every alert. They are also how Alertmanager deduplicates an HA pair: two identical Prometheus servers send identical alerts, and identical external labels make them collapse into one notification.
Prometheus is deliberately stupid about routing. Grouping,
inhibition, silences, receiver selection and notification cadence
(group_wait, group_interval, repeat_interval) all live in
Alertmanager. If the wrong person got paged, the bug is almost
never in the rule file.
Evaluation limits
Every rule evaluation is a PromQL query and obeys the server-wide
limits: --query.timeout (default 2m) and --query.max-samples
(default 50 million). A rule that hits them fails that evaluation:
prometheus_rule_evaluation_failures_total increments and the
alert state stays whatever it was.
The subtler failure: if a whole group cannot finish within its
interval, iterations are skipped.
prometheus_rule_group_iterations_missed_total climbs and
prometheus_rule_group_last_duration_seconds exceeds the interval.
Your one-minute alert quietly became a two-minute alert, and
nothing tells you unless you look.
How to configure it
Wiring in prometheus.yml:
rule_files:
- '/etc/prometheus/rules/*.yml'
alerting:
alert_relabel_configs:
# never send info-severity alerts to the pager stack at all
- source_labels: [severity]
regex: 'info'
action: drop
alertmanagers:
- static_configs:
- targets: ['alertmanager-01:9093', 'alertmanager-02:9093']
timeout: 10s # give up on a slow Alertmanager
How to validate it
# Syntax and semantics of a rule file before it ever reaches prod
promtool check rules /etc/prometheus/rules/node.yml
# Checking /etc/prometheus/rules/node.yml
# SUCCESS: 12 rules found
# Unit-test rules against synthetic series (CI-friendly)
promtool test rules /etc/prometheus/rules/tests/node.test.yml
# What is loaded and healthy, right now
curl -s localhost:9090/api/v1/rules \
| jq '.data.groups[] | {name, interval,
rules: [.rules[] | {name, health, lastError}]}'
# What is pending or firing, right now
curl -s localhost:9090/api/v1/alerts \
| jq '.data.alerts[] | {alert: .labels.alertname, state: .state}'
And the PromQL view of pipeline health:
ALERTS{alertstate="firing"} # live alerts
prometheus_rule_evaluation_failures_total # should be flat
prometheus_rule_group_last_duration_seconds # vs the group interval
prometheus_rule_group_iterations_missed_total # should be flat
prometheus_notifications_errors_total # Alertmanager sends failing
prometheus_notifications_dropped_total # queue full; alerts lost
How it can fail
- A broken rule file at reload. The old rules keep running,
prometheus_config_last_reload_successfulflips to 0, and the new alert simply never appears in/api/v1/rules. Silent unless you alert on the reload metric. - An expression too expensive for its interval. Symptom:
last_durationabove the group interval, missed iterations climbing, alerts firing minutes late. formisjudged in either direction.for: 0on a noisy metric flaps and pages twice a night;for: 15mon a disk-filling-fast alert pages after the disk is full.foris the trade-off knob, not a formality.alert_relabel_configsdrops or rewritesseverity. Symptom: every alert lands on the default route — or nowhere, if the default route is a blackhole.- Alertmanager unreachable and the queue fills. Symptom:
alerts firing in
/api/v1/alerts, nothing paging,prometheus_notifications_dropped_totalgrowing. - HA pair with mismatched
external_labels. Alertmanager cannot deduplicate what does not look identical; every firing alert pages twice.
How to troubleshoot it
- Is the rule loaded and healthy?
/api/v1/rules— checkhealthandlastErrorbefore anything else. - Is it evaluating on time? Group
last_durationagainst the interval, plusiterations_missed. - What state is the alert in?
/api/v1/alertsand theALERTSseries. Pending forever usually means the condition is flapping across the threshold and resettingfor. - Did the notification leave Prometheus? The notifications
metrics above; then the Alertmanager side — its API, its logs,
amtool config routes showfor where the alert would land. - Only then start doubting the expression itself.
Security implications
- Write access to rule files is effectively production access: rules decide who gets woken up and when. Ownership and review should match that.
- Annotations template label values into notification text. If a secret ever leaks into a label (lesson 02), it lands in Slack or PagerDuty verbatim.
- Alertmanager targets take credentials in
prometheus.yml; the Alertmanager API itself lets anyone who can reach it create silences. Restrict both.
Performance implications
Rule cost is rule count × query cost ÷ interval, paid on the same query engine as dashboards. Recording rules are the lever: they convert an expensive per-viewer query into a cheap stored series, at the price of ingestion and storage. Keep group intervals at or above the group’s slowest evaluation with headroom; a group that cannot keep up does not degrade gracefully — it skips.
Verification
You should now be able to answer:
- What does a recording rule produce that an alerting rule does not, and when would you choose each?
- What exactly does
formeasure, and what resets it? - Where do grouping, inhibition and silences live — and what must Prometheus attach for an HA pair to deduplicate?
- Which three metrics tell you rule evaluation itself is unhealthy?
- Why does “no alerts in
/api/v1/alerts” not prove the pipeline is working?
Quiz
Knowledge check · 8 questions
Q1. What does for: 5m mean on an alerting rule?
Q2. What resets a pending alert back to inactive?
Q3. Grouping, silences and inhibition are responsibilities of the Prometheus server.
Q4. A rule group takes 90 seconds to evaluate with a 60-second interval. What happens?
Q5. Which observations together confirm an alert is firing end-to-end?
Q6. promtool test rules can unit-test alerting rules against synthetic input series.
Q7. An HA pair of Prometheus servers double-pages for every alert. What is the likely cause?
Q8. Name the synthetic metric Prometheus maintains for every active alert, carrying the alertstate label.
Passing score: 75%. Answers are checked in this browser.