ObservabilityXXI · Alert InhibitionAlertInhibition
Dependency-Aware Alerting
What you'll learn
- Explain how a service catalog becomes dependency labels on alerts
- Write dependency-aware inhibit rules using depends_on and depends_on_team labels
- Distinguish a blackout (target suppressed entirely) from a demote (target severity reduced)
- Recognise the failure modes of a stale or incorrect dependency map
- Apply the design pattern to a real dependency tree in your environment
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 production database goes down at 14:32. The database alert
fires within seconds — DatabaseDown. Thirty seconds later the
pager begins to receive a cascade: CheckoutServiceError,
CartServiceError, PaymentServiceError, AuthServiceError.
Eight alerts for one root cause. The on-call database
administrator is paging about a database; every dependent
service team is paging about their service.
With a dependency map, the cascade collapses. The database alert inhibits every dependent-service error alert; the on-call database engineer is the only person paged; the dependent-service teams see their alerts suppressed with a note that the cause is upstream. When the database recovers, the dependent services recover automatically.
This lesson covers the discipline of turning a service catalog into a set of labels, and the Alertmanager rules that use those labels to suppress cascades. The discipline has two design decisions: how to encode the dependency tree as labels, and how to choose between blackout (suppress entirely) and demote (reduce severity) for each dependent alert.
What it is
Dependency-aware alerting is the practice of carrying the dependency tree of a system on every alert as a set of labels, and writing Alertmanager rules that suppress or demote alerts based on whether an upstream is also alerting.
Dependency tree (from the service catalog):
postgres-prod
|
+-- checkout-svc
+-- cart-svc
+-- payment-svc
+-- auth-svc
Labels on every alert produced by these services:
service="checkout-svc"
depends_on="postgres-prod"
depends_on_team="data-platform"
team="checkout"
The depends_on label encodes the immediate dependency. The
depends_on_team label encodes the team that owns the
dependency — the team that should be paged when the
dependency itself fails.
Why a sysadmin cares
The production failure shape that dependency-aware alerting prevents is the cascading incident. A single failure (a database, a load balancer, an authentication service) causes a wave of dependent failures, each of which is independently correct as an alert. The on-call rota receives N alerts for one fix. The operator must triage N alerts to find the one that matters. The mean time to identify the root cause rises with N.
A secondary failure shape is demote. Some dependent
failures are not worth paging on. If the database is down,
the AuthService returning 503s is not actionable — the
operator cannot fix auth until the database is back. The
discipline is to demote the dependent alert from warning
to info for the duration of the upstream alert. The
dependent alert is still recorded; it is just not paged.
How it works
There are two distinct design decisions.
Decision 1: blackout vs demote
Blackout : suppress the dependent alert entirely
Demote : reduce the dependent alert severity
(e.g. warning -> info)
Blackout is the right choice when the dependent alert carries
no information beyond “the upstream is broken.” If the
database is down, every ServiceErrorRateHigh on a dependent
service is a direct consequence; the operator cannot act on
the dependent service until the upstream is restored.
Demote is the right choice when the dependent alert carries
information that is still useful during an upstream outage.
For example, a LatencyHigh on a service whose upstream is
slowing is informative — the operator needs to know which
services are degraded even before the upstream fully fails.
Demoting means “still record, do not page.”
Decision 2: how to encode the dependency
Three common patterns:
Pattern A — depends_on (label on every alert):
service="checkout-svc"
depends_on="postgres-prod"
Inhibit rule:
source_matchers: [service="postgres-prod"]
target_matchers: [depends_on="postgres-prod"]
equal: [cluster]
Pattern B — depends_on_team (label on every alert):
service="checkout-svc"
depends_on_team="data-platform"
Inhibit rule:
source_matchers: [team="data-platform", severity="critical"]
target_matchers: [depends_on_team="data-platform"]
equal: [cluster]
Pattern C — combined:
service="checkout-svc"
depends_on="postgres-prod"
depends_on_team="data-platform"
Pattern A is the most precise — the rule fires only when the exact dependency is firing. Pattern B is the broadest — the rule fires for any alert owned by the dependency’s team. Pattern C is the most flexible — the precision of A and the breadth of B are both available.
Under the hood
How to configure it
A complete configuration for the four-service dependency tree above. Three rules: a per-dependency rule (Pattern A), a team-wide rule (Pattern B), and a demote rule that reduces severity rather than suppressing.
# /etc/alertmanager/alertmanager.yml
route:
receiver: default
group_by: [service, alertname, instance]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
inhibit_rules:
# 1. Per-dependency blackout (Pattern A).
# When postgres-prod is firing critical, suppress every
# dependent service's error alerts on the same cluster.
- source_matchers:
- service="postgres-prod"
- severity="critical"
target_matchers:
- depends_on="postgres-prod"
- severity=~"warning|critical"
equal: [cluster]
# 2. Per-team blackout (Pattern B).
# When data-platform owns something critical, suppress
# every alert whose team depends on data-platform.
- source_matchers:
- team="data-platform"
- severity="critical"
target_matchers:
- depends_on_team="data-platform"
- severity="warning"
equal: [cluster]
# 3. Demote (Pattern A, demote variant).
# When the upstream is slow (warning, not critical),
# demote dependent latency alerts from warning to info
# by suppressing the warning alert entirely. The info
# alert (a separate recording rule with lower threshold)
# is allowed through.
- source_matchers:
- service="postgres-prod"
- alertname=~"^(PostgresLatencyHigh|PostgresConnectionsHigh)$"
target_matchers:
- depends_on="postgres-prod"
- alertname=~"^ServiceLatencyHigh$"
- severity="warning"
equal: [cluster]
receivers:
- name: default
webhook_configs:
- url: 'http://localhost:5001/alerts'
Three things to read carefully:
- Equal is
cluster. Suppression spans every instance in the cluster. If the dependency is regional (e.g. a primary database per region), addregionto the equal clause. - Rule 3 is a demote. The source is
warning(notcritical), and the target is the warning-levelServiceLatencyHigh. The warning alert is suppressed; the info-level alert (a separate recording rule) is unaffected. The dependent team still sees the latency signal in their dashboards, but is not paged. severityis on both source and target. Without the target severity filter, an upstreaminfoalert would suppress a dependentcriticalalert. The filter ensures the source is at least as severe as the target.
How to validate it
Five checks. The first three are static. The last two are live.
# 1. Static syntax check (CONFIGURATION severity)
amtool check-config /etc/alertmanager/alertmanager.yml
Expected output (illustrative):
Checking /etc/alertmanager/alertmanager.yml
SUCCESS
Parsed and loaded successfully
Found inhibit rule 'postgres-down-suppresses-dependents'
Found inhibit rule 'data-platform-down-suppresses-dependents'
Found inhibit rule 'postgres-slow-demotes-dependent-latency'
# 2. SIGHUP reload (CONFIGURATION severity)
kill -HUP "$(pidof alertmanager)"
# 3. Loaded-config echo (READ-ONLY). The status API returns
# the running configuration as a YAML string in
# .config.original, not as a parsed object.
curl -s http://localhost:9093/api/v2/status \
| jq -r '.config.original'
Expected output (illustrative): the three inhibit_rules
entries from the file, echoed back as YAML.
# 4. Synthetic cascade dry-run (READ-ONLY)
amtool alert add alertname=PostgresDown \
service="postgres-prod" team="data-platform" \
cluster="prod-eu" severity="critical"
amtool alert add alertname=CheckoutServiceErrorRateHigh \
service="checkout-svc" team="checkout" \
depends_on="postgres-prod" depends_on_team="data-platform" \
cluster="prod-eu" severity="warning"
amtool alert add alertname=AuthServiceErrorRateHigh \
service="auth-svc" team="platform" \
depends_on_team="data-platform" \
cluster="prod-eu" severity="critical"
sleep 2
curl -s 'http://localhost:9093/api/v2/alerts?active=true&silenced=true' \
| jq '.[] | {alertname: .labels.alertname,
service: .labels.service,
status: .status.state,
inhibitedBy: .status.inhibitedBy}'
Expected output (illustrative):
{ "alertname": "PostgresDown",
"service": "postgres-prod",
"status": "active",
"inhibitedBy": [] }
{ "alertname": "CheckoutServiceErrorRateHigh",
"service": "checkout-svc",
"status": "suppressed",
"inhibitedBy": ["PostgresDown"] }
{ "alertname": "AuthServiceErrorRateHigh",
"service": "auth-svc",
"status": "suppressed",
"inhibitedBy": ["PostgresDown"] }
Both dependents are suppressed by rule 1 (per-dependency). Rule 2 (per-team) also fires but the result is the same.
amtool has no command that deletes an alert. Alerts leave
Alertmanager by resolving, not by being removed: an alert
posted without an explicit end time resolves on its own after
the global resolve_timeout (five minutes by default). To
resolve one immediately, re-post the same label set with a
start and an end that are both in the past.
# Clean up: resolve the three synthetic alerts.
START="$(date -u -d '-5 minutes' +%Y-%m-%dT%H:%M:%SZ)"
END="$(date -u -d '-1 minute' +%Y-%m-%dT%H:%M:%SZ)"
amtool alert add alertname=PostgresDown \
service="postgres-prod" team="data-platform" \
cluster="prod-eu" severity="critical" \
--start="$START" --end="$END"
amtool alert add alertname=CheckoutServiceErrorRateHigh \
service="checkout-svc" team="checkout" \
depends_on="postgres-prod" depends_on_team="data-platform" \
cluster="prod-eu" severity="warning" \
--start="$START" --end="$END"
amtool alert add alertname=AuthServiceErrorRateHigh \
service="auth-svc" team="platform" \
depends_on_team="data-platform" \
cluster="prod-eu" severity="critical" \
--start="$START" --end="$END"
amtool alert query alertname=PostgresDown
# 5. Negative case — alert from a service that does NOT
# depend on postgres (READ-ONLY)
amtool alert add alertname=PostgresDown \
service="postgres-prod" cluster="prod-eu" \
severity="critical"
amtool alert add alertname=SearchServiceErrorRateHigh \
service="search-svc" depends_on="opensearch" \
cluster="prod-eu" severity="warning"
sleep 2
curl -s 'http://localhost:9093/api/v2/alerts?active=true&silenced=true' \
| jq '.[] | select(.labels.alertname=="SearchServiceErrorRateHigh")
| {status: .status.state,
inhibitedBy: .status.inhibitedBy}'
Expected output: status: "active", inhibitedBy: [].
The search service does not depend on postgres and is not
suppressed.
How it can fail
Five failure modes specific to dependency-aware alerting:
- Stale dependency map. A service was migrated off
Postgres six months ago, but the
depends_onlabel still sayspostgres-prod. Symptom: the migrated service is silently suppressed whenever Postgres fires; the operator does not know. Investigation: compare the dependency map in the service catalog against the labels on every active alert. - Wrong direction. A rule says “when checkout-svc fires, suppress cart-svc” but the actual dependency is the reverse (cart-svc is upstream of checkout-svc). Symptom: during a cart outage, checkout errors are suppressed and the checkout team is not paged about the real cause. Investigation: diff every inhibit rule against the service catalog and confirm the direction of every dependency.
- Cascade depth unaccounted for. The dependency is
A -> B -> C, but the rule only handles the direct edgeA -> B. Symptom: whenAis down,Bis suppressed butCis still paged as a downstream consequence. Investigation: trace the dependency tree from the source to the leaves and confirm every edge is covered. - Demote rule swallows a real signal. The demote rule
suppresses the
warningalert but acriticalalert on the same label set is the one the operator needed. Symptom: during a slow upstream, the critical alert fires but is suppressed by a rule intended for the warning. Investigation: confirm the target severity filter is present and is at least as high as the source severity. - Cyclical dependency. Two services declare each other as dependencies. Each rule references the other. Symptom: every alert on either side suppresses the other; nothing reaches the pager. Investigation: walk the dependency tree and look for cycles; the service catalog should reject them at write time.
How to troubleshoot it
The diagnostic order for a missing or wrong suppression.
- Is the source firing with the expected labels?
curl /api/v2/alerts?active=truefor the source. Confirm the labels the rule’s source_matchers are looking for are actually present. A source alert missing thedepends_on_teamlabel will not match a Pattern B rule. - Is the target firing with the expected labels? Same query, for the target. Confirm the labels the rule’s target_matchers are looking for are present. The most common cause of “the rule is not firing” is the target missing a label the author assumed was present.
- Is the equal clause satisfiable? Diff the labels on
source and target. For every label in
equal:, the value must be byte-identical. Theclusterlabel is the usual suspect (prod-euvsprod_eu,prodvsProd). - Is the dependency map current? Compare the
depends_onanddepends_on_teamlabels on every active alert against the service catalog. Any mismatch is a candidate root cause. - Read the alertmanager log. Run with
--log.level=debug. The lines aroundmatched sourceandmatched targetshow the alert pairs that were considered and the reason each was or was not suppressed.
Security implications
The dependency map exposes the topology of the system. A service catalog that lists every internal service and every team’s dependencies is a complete map of the platform. Treat the catalog and the alert labels as production-sensitive configuration. Repository access controls apply; the Prometheus rules repo is not a public artefact.
The Alertmanager API exposes every alert label, including
depends_on_team. Restrict API access to the loopback
interface or a reverse proxy with authentication.
Performance implications
The cost of a dependency-aware rule is the same as a basic
rule per evaluation. The new cost is the cost of maintaining
the dependency map. Every service that is added, retired,
or re-platformed requires a corresponding label change in
every Prometheus rule that emits alerts for the service.
Treat the label maintenance as part of the service’s
definition-of-done: a new service is not “deployed” until
its alerts carry the right depends_on and
depends_on_team labels.
Production guidance
- Maintain the dependency map in one place. The service catalog is the source of truth. Every Prometheus rule imports it.
- Cover the full cascade. If
A -> B -> C, every edge is a rule. The leaves still need to be reachable by an upstream alert. - Distinguish blackout from demote explicitly. The rule’s intent — “suppress entirely” or “reduce severity” — should be obvious from its name in the configuration.
- Reject cyclical dependencies at the catalog level. An Alertmanager rule can be written for either direction; a cycle means neither rule is right.
- Review the rules when the dependency map changes. The last lesson in this module defines the rubric.
Verification
You should now be able to answer:
- What is the difference between a blackout and a demote in dependency-aware alerting?
- How does a service catalog become a set of labels on alerts?
- Why is the direction of every dependency explicit in the rule, and what happens when the direction is wrong?
- How do you prove the cascade is being suppressed end-to-end through the dependency tree?
Quiz
Knowledge check · 8 questions
Q1. What is the difference between a blackout and a demote?
Q2. Which labels would you put on alerts produced by a service whose direct dependency is a database?
Q3. A dependency tree is A -> B -> C. The rule handles only the direct edge A -> B. What happens when A is down?
Q4. A stale depends_on label makes an inhibit rule fire too often against unrelated alerts.
Q5. Name the pattern that uses a single label to encode the team that owns the upstream dependency.
Q6. A demote rule suppresses ServiceLatencyHigh (warning) when PostgresLatencyHigh (warning) fires. What happens to a separate ServiceLatencyHigh at severity=critical fired by the same service?
Q7. Two services declare each other as dependencies. What is the failure shape?
Q8. Dependency-aware alerting requires the dependency map to be maintained as a first-class operational artefact.
Passing score: 75%. Answers are checked in this browser.