ObservabilityXIX · AlertmanagerAlertmanager
Inhibition
What you'll learn
- Read an `inhibit_rule:` block and predict which alerts will be suppressed
- Design a Critical-inhibits-Warning rule using `equal:` to constrain the scope
- Distinguish a firing source alert from a pending source alert in inhibition
- Verify that an inhibition rule is doing what it should using `amtool config routes test`
- Diagnose the common gotcha: an inhibition rule that swallows a real alert
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
A whole database cluster goes dark. ClusterDown fires.
Immediately after, every host-level rule fires:
HostDown on each of the eight database hosts,
PostgresReplicationLag on each replica,
DatabaseConnectionPoolExhausted on the primary,
BackupFailed on the backup job. Eight hosts, four alert names,
thirty-two firing alerts. One root cause.
Inhibition is the rule that says: when ClusterDown is firing,
none of the host-level alerts get a notification. The
ClusterDown page tells the on-call the right story. The
host-level alerts are still in nflog for the postmortem — they
are just suppressed as notifications.
What it is
Inhibition is the mechanism by which Alertmanager silences one
set of alerts when another set is firing. It is configured under
the top-level inhibit_rules: key and applies globally across
all routes. Each rule has:
source_matchers:(orsource_match:) — the matchers that identify the cause alert. When an alert matching these is firing, it can suppress other alerts.target_matchers:(ortarget_match:) — the matchers that identify the consequence alerts that should be silenced while the source is firing.equal:(optional) — a list of label names that must have the same value on both the source and the target for the inhibition to apply. The single most important lever for avoiding accidental scope.
Inhibition runs after grouping, before notification. The target
alert is still present in nflog/ and is still visible in the
UI; it is simply excluded from notifications while a matching
source alert is firing.
Why a sysadmin cares
Inhibition is the highest-leverage / highest-risk knob in the
whole alertmanager.yml. The leverage is real: a well-designed
inhibit rule converts a thirty-two-page flood into a single
focused page. The risk is also real: an inhibition rule with a
missing or wrong equal: clause can swallow a real incident.
Two failure shapes appear repeatedly:
- The ClusterDown page hides a real, independent HostDown.
A switch reboots. The whole cluster goes dark. The
ClusterDownpage is right and useful. Meanwhile, an unrelated host on the same cluster has a hardware failure that would normally page a different on-call. The HostDown alert is inhibited by the cluster-level alert. The hardware failure goes unnoticed until the next morning’s health check. - The Critical-inhibits-Warning rule swallows a unique
critical. A
severity=criticalsource matcher inhibits allseverity=warningtargets. The intended behaviour is “if the critical page is already out, don’t spam warnings.” The unintended behaviour is that any critical, on any host, silences warnings across the fleet. A critical on the edge router silences warnings on the database cluster.
Both shapes are the same mistake: missing or wrong equal:
constraints. The discipline is to write the equal: clause
before writing the matcher clause, and to treat any
inhibit_rule without equal: as a code review blocker.
How it works
alert X: alertname=ClusterDown cluster=prod-eu-1 severity=critical (firing)
alert Y: alertname=HostDown cluster=prod-eu-1 severity=critical (firing)
alert Z: alertname=DiskFillingSoon cluster=prod-eu-1 severity=warning (firing)
inhibit_rule:
source_matchers: [alertname=ClusterDown]
target_matchers: [severity=critical]
equal: [cluster]
evaluation for Z:
source matches ClusterDown (alert X)?
yes, X is firing with cluster=prod-eu-1
target matches severity=critical?
Z is severity=warning, NO MATCH
Z is NOT inhibited.
evaluation for Y:
source matches ClusterDown (alert X)?
yes, X is firing with cluster=prod-eu-1
target matches severity=critical?
Y is severity=critical, MATCH
equal: cluster=prod-eu-1 == cluster=prod-eu-1? YES
Y IS inhibited.
The rule above inhibits critical host-level alerts while the cluster is down — but only inside the same cluster. A critical on the edge router does not inhibit a warning on the database cluster because the cluster label differs.
The equal: clause is the boundary. Without it, the rule
matches across the whole fleet.
Under the hood
Inhibition runs as a pass over the active alerts in nflog. For
each (source, target) pair, AM checks:
- Is the source alert firing? (A pending source alert does not inhibit.)
- Do the source matchers match the source alert?
- Do the target matchers match the target alert?
- For every label in
equal:, is the value the same on both?
The cost is O(sources x targets) per inhibit_rule per notification cycle. With a handful of rules and a few hundred firing alerts, the cost is negligible.
A subtle detail: a source alert is only a source if it is
firing — not pending, not inactive. An alert that has been
pending for 30 seconds and has not yet entered firing does
not inhibit anything. This matters when you tune for: in the
rule definitions: a long for: on the source delays the
inhibition window.
How to configure it
A production-grade set of inhibit rules, scoped by equal::
inhibit_rules:
# Critical pages silence warnings inside the same cluster.
# Use this to prevent the "one root cause, thirty pages" shape.
- source_matchers:
- severity = critical
target_matchers:
- severity =~ "warning|info"
equal: ['cluster', 'alertname']
# ClusterDown silences host-level alerts inside the same cluster.
- source_matchers:
- alertname = ClusterDown
target_matchers:
- alertname =~ "HostDown|ServiceDown|InstanceUnreachable"
equal: ['cluster']
# DatabaseClusterDown silences database host alerts inside the
# same database cluster.
- source_matchers:
- alertname = DatabaseClusterDown
target_matchers:
- alertname =~ "PostgresReplicationLag|PostgresConnectionsExhausted|BackupFailed"
equal: ['dbcluster']
# NetworkPartitionDown silences application-level latency alerts
# that ride on the same network path.
- source_matchers:
- alertname = NetworkPartitionDown
target_matchers:
- alertname =~ "HighRequestLatency|HighErrorRate"
equal: ['region']
Two patterns to read off these rules:
equal:always carries at least one label. The first rule usesclusterandalertnameso that a critical on one alert does not silence warnings on a different alert in the same cluster.- Each rule is a parent/child relationship with a scope. The
rule does not say “inhibit this name.” It says “when this
parent fires, silence this child inside this scope.” The
scope is the
equal:label.
The right discipline is to enumerate the alerts, identify the
parent/child relationships, and write a rule per relationship
with the equal: clause that bounds the scope to where the
parent and child are causally related.
How to validate it
amtool config routes test does not directly simulate
inhibition. The right approach is to:
- Fire a real source alert (or simulate with
amtool alert add). - Fire a real target alert (or simulate).
- Confirm the target is suppressed via the AM log and via
GET /api/v2/alerts.
# 1. Fire the source alert.
amtool --alertmanager.url=http://localhost:9093 alert add \
alertname=ClusterDown cluster=prod-eu-1 severity=critical
# Output: component=active stage=active alerts=[ClusterDown]
# 2. Fire the target alert.
amtool --alertmanager.url=http://localhost:9093 alert add \
alertname=HostDown cluster=prod-eu-1 instance=db-1 severity=critical
# Output: component=active stage=active alerts=[HostDown, ClusterDown]
# 3. Inspect via the API. The target is still in nflog but
# its status is "suppressed" while the source is firing.
curl -s http://localhost:9093/api/v2/alerts \
| jq '.[] | {labels: .labels, status: .status.state}'
# Output:
# {
# "labels": {"alertname": "ClusterDown", "cluster": "prod-eu-1", ...},
# "status": "active"
# }
# {
# "labels": {"alertname": "HostDown", "cluster": "prod-eu-1", ...},
# "status": "suppressed"
# }
# 4. Resolve the source. The target should become active.
amtool --alertmanager.url=http://localhost:9093 alert add \
alertname=ClusterDown cluster=prod-eu-1 severity=critical \
--end
The visible state is the truth. status.state == "suppressed"
is the unambiguous signal that inhibition is working. If the
target stays active, the rule is misconfigured — most likely
the equal: clause is wrong or the matchers do not match the
alert labels.
How it can fail
The recurring failure modes, in descending order of operational cost:
- Missing
equal:clause. A rule of the formsource: severity=critical;target: severity=warningwith noequal:silences every warning on the fleet whenever any critical fires. A critical on the cache layer silences the disk-full warning on the database. The fix is to requireequal: [cluster, alertname](or similar) on every rule. equal:carries the wrong label. A rule usesequal: [region]when the parent/child relationship is scoped bycluster. A critical incluster=prod-eu-1inhibits warnings across the wholeregion=eu-westregion. Always pick the smallest scope that captures the causal relationship.- Source alert is
pending, notfiring. A longfor:on the source rule delays when the inhibition kicks in. In the interim, the targets fire and notify. Tunefor:on the source down to a few seconds for the relationship to feel immediate. - The source rule never fires. A typo in the source matcher
(
alertname = ClusterDwon) means the rule never matches. Targets fire and notify normally. The intended inhibition never happens. Validate withamtool config routes testand confirm against the live API. - Source alert suppressed by another rule. Inhibition rules
do not chain through other inhibition rules in a way that
re-suppresses targets. If
ClusterDownis itself suppressed by a higher-level rule, the host-level alerts are not inhibited. Verify the firing state of the source via theapi/v2/alertsendpoint, not via the visible notifications. - Inhibit rule scope matches a sibling, not a child. A
parent alert that is firing on a sibling scope (different
cluster) inhibits a target that happens to share theseveritylabel. The fix is to add the rightequal:label. Always validate by firing two alerts on different scopes and confirming the target is not suppressed.
How to troubleshoot it
When a real alert is missing because of inhibition, the diagnostic order:
- Confirm the alert is in
nflog.curl -s http://localhost:9093/api/v2/alerts | jq '.[] | select(.labels.alertname == "HostDown")'. If it is not present, the rule never fired in Prometheus. - Inspect
status.state. A state ofsuppressedconfirms inhibition is the cause. A state ofactivemeans the notification path is broken elsewhere. - Identify the source alert. Filter
nflogforseverity = critical(or whatever the source matchers are). If no source is firing, the target should not be inhibited. - Walk the inhibit rule by hand. For each inhibit_rule, check the four conditions: source firing, source matchers, target matchers, equal labels.
- Tail the AM log. The line
component=inhibitor ...shows the evaluation. Cross-check against the expected behaviour.
Security implications
Inhibition is not security-sensitive on its own, but two adjacent risks matter:
- Suppression as a denial-of-service surface. An attacker who can fire an arbitrary alert (via a Prometheus instance with a permissive alert rule, or via a misconfigured webhook receiver that accepts alerts) can suppress legitimate notifications by firing the source matcher. The control is the same as for any alert source: only trusted sources can push alerts into the pipeline.
- Information disclosure in inhibited alerts. An inhibited
alert is still in
nflogand visible to anyone with API access. A high-severity security alert that is inhibited by a routine cluster-down alert is still visible to operators with the right permissions; the concern is that non-operators may also see it via the receiver if the inhibition rule is wrong.
Performance implications
Inhibition is a single linear pass over the active alerts per
rule per notification cycle. The cost is negligible for fleets
of a few thousand firing alerts. The failure shape is a rule
with extremely broad source matchers (e.g. severity = .*)
that matches every alert — the cost becomes quadratic in the
number of firing alerts. Treat any matcher with =~ covering
many values as a smell.
Production guidance
- Every inhibit_rule carries an
equal:clause. Make it a lint rule in CI:amtool check-configplus a wrapper that parses the YAML and asserts theequal:key is present. - Prefer narrow source matchers. The source should be a
specific alert name, not a severity.
alertname = ClusterDownis better thanseverity = critical. - Validate with synthetic alerts. A small library of
amtool alert addcommands that exercise every inhibit rule should run in CI after every config change. The expected status of each target alert should besuppressedin the assertions. - Document the parent/child relationships. A diagram of the inhibition tree in the instrumentation guide is the cheapest way to keep the relationships straight when the rules grow.
- Audit quarterly. Enumerate all inhibit_rules and confirm each source matcher still corresponds to a real, firing alert. Decommissioned alert names that remain in inhibit rules are silent no-ops; missing names are silent failures.
Verification
You should now be able to answer:
- What are the four conditions that must all be true for inhibition to apply?
- Why is a missing
equal:clause a production failure mode? - What is the difference between a firing source and a pending source in inhibition?
- How do you confirm via the API that an alert is being suppressed by inhibition rather than missing?
- What is the right discipline for designing the
equal:clause on a new inhibit rule?
Quiz
Knowledge check · 8 questions
Q1. Which of the four conditions must be true for inhibition to apply?
Q2. An inhibit_rule with source=severity=critical, target=severity=warning, no equal clause:
Q3. A pending source alert can inhibit target alerts.
Q4. Which labels are reasonable choices for the equal clause on a Critical-inhibits-Warning rule?
Q5. Which alert status in the v2 API confirms an alert is being inhibited?
Q6. A host-level alert is missing from notifications during a cluster outage. The most likely cause is:
Q7. You want to silence HostDown alerts only when ClusterDown fires on the same cluster. Which equal clause is correct?
Q8. Inhibition rules can chain through other inhibition rules to suppress targets.
Passing score: 75%. Answers are checked in this browser.