AlertManager
Prometheus decides what is wrong; AlertManager decides who hears about it, how often, and whether it's worth waking someone up. It sits between rule evaluation and a human's phone — deduplicating identical alerts, grouping related ones into a single notification, routing by label match, and suppressing noise via inhibition and silences.
1. Architecture
Prometheus evaluates rules and pushes firing alerts to AlertManager, which deduplicates, groups, routes, and dispatches notifications.
flowchart LR
P[Prometheus] -->|POST /alerts| AM[AlertManager]
AM --> RT{Routing Tree}
RT -->|severity=critical| PD[PagerDuty]
RT -->|severity=warning| SL[Slack]
RT -->|default| EM[Email]
Prometheus pushes a firing alert to AlertManager. What four things happen to it before a human sees a notification?
2. Alert Lifecycle
An alert transitions through states based on the for: duration in the rule and whether it resolves.
stateDiagram-v2
[*] --> Inactive: rule not firing
Inactive --> Pending: condition true
Pending --> Firing: for-duration met
Pending --> Inactive: condition false
Firing --> Resolved: condition false
Resolved --> [*]
| State | Meaning |
|---|---|
| Inactive | Rule condition is false |
| Pending | Condition true, waiting for: duration |
| Firing | Duration exceeded — alert sent |
| Resolved | Condition cleared, resolve notification sent |
for: duration, but AlertManager hasn't heard anything yet.
for: duration has been met. Prometheus finally sends the alert to AlertManager — this is the entry point into everything else on this page: grouping, routing, dispatch.
A rule's condition goes true, then false again 20 seconds later, and the rule has for: 2m. Does AlertManager ever see this alert?
for: duration was met, so the alert went Pending → Inactive without ever reaching Firing — and Firing is the only state that gets sent to AlertManager.3. Routing Tree
Routes are evaluated top-down; first match wins. Each route can override receiver, group_by, and timing.
flowchart TD
G[global defaults] --> R[root route]
R --> M1{team=infra?}
M1 -->|yes| R1[pagerduty-infra]
M1 -->|no| M2{severity=warning?}
M2 -->|yes| R2[slack-warning]
M2 -->|no| R3[default receiver]
receiver is the fallback if nothing more specific ever matches.
team=infra? Since it's evaluated top-down, this match is tried before anything else — a match here sends the alert to pagerduty-infra and evaluation stops.
team=infra didn't match, the tree moves to the next sibling route: severity=warning? A match here sends it to slack-warning.
The routing tree checks team=infra? before severity=warning?. An alert arrives with both team=infra and severity=warning set. Which receiver gets it?
pagerduty-infra. Routes are evaluated top-down and first match wins — since the team=infra check comes first in the tree, it matches and stops evaluation before the severity=warning check is ever reached.Config example:
route:
receiver: default-receiver
group_by: [alertname, cluster]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- matchers:
- severity = critical
receiver: pagerduty-critical
continue: false
- matchers:
- severity = warning
receiver: slack-warning
4. Grouping
| Parameter | Purpose | Typical Value |
|---|---|---|
group_by |
Labels that form a notification group | [alertname, cluster, namespace] |
group_wait |
Wait before sending first notification | 30s |
group_interval |
Wait before sending added/resolved alerts | 5m |
repeat_interval |
Re-notify if still firing | 4h |
Grouping prevents alert storms: 100 pods failing → 1 grouped notification.
sequenceDiagram
participant P as Prometheus
participant AM as AlertManager
participant R as Receiver
P->>AM: Alert A fires (group: cluster=prod)
Note over AM: group_wait timer starts (30s)
P->>AM: Alert B fires, same group
Note over AM: group_wait elapses, batch A+B
AM->>R: notify (A, B)
P->>AM: Alert C fires, same group
Note over AM: waits group_interval (5m) before adding new alerts
AM->>R: notify (A, B, C)
Note over AM: A, B, C still firing after repeat_interval (4h)
AM->>R: re-notify (A, B, C)
group_wait clock (typically 30s), giving related alerts a chance to land in the same batch.
group_by.
group_interval (typically 5m) before sending an updated notification that includes the new alert.
repeat_interval (typically 4h) with no new alerts, AlertManager re-sends the same notification as a reminder.
100 pods fail at once, all sharing the same group_by labels. Roughly how many notifications land in someone's inbox, and why?
group_by: alerts sharing the same group labels get batched into a single notification instead of paging once per alert — this is exactly what prevents alert storms.5. Inhibition
Suppress derived/symptom alerts when the root cause is already firing. Inhibition rules match a source alert and silence matching target alerts.
inhibit_rules:
- source_matchers:
- alertname = "NodeDown"
target_matchers:
- alertname =~ "Pod.*"
equal: [cluster, node]
If NodeDown fires for node=worker-1, all Pod* alerts on the same node are silenced.
NodeDown fires for node=worker-2. Fifteen PodCrashLooping alerts fire on worker-2, and one more fires on healthy worker-5. Which of these get silenced by inhibition?
worker-2. Inhibition matches a source alert (NodeDown) against targets (Pod*) using the equal labels — here node — so only targets sharing that label with a firing source get suppressed. The worker-5 alert has no matching source firing, so it still pages.6. Silences
Silences mute alerts matching a set of matchers for a time window. Created via UI or amtool.
Types:
| Type | Example use |
|---|---|
| Time-based | Maintenance window (Sat 02:00–04:00) |
| Matcher-based | Mute specific service during deploy |
# Create silence for 2 hours on a specific service
amtool silence add alertname="HighErrorRate" service="payments" \
--duration=2h --comment="Deploying payments v2.3"
# List active silences
amtool silence query
# Expire a silence
amtool silence expire <silence-id>
What two ways can you create a silence in AlertManager?
amtool CLI (for example amtool silence add ... --duration=2h).Grouping, inhibition, and silencing all reduce notification noise, but they solve different problems — worth keeping straight:
group_by labels into one notification.Trigger: Automatic, based on labels — no manual rule needed beyond
group_by.Goal: Fewer notifications for the same incident (100 pods failing → 1 notification), not fewer alerts.
NodeDown silences that node's Pod* alerts).Trigger: A matching source alert has to actually be firing right now.
Goal: Hide symptom alerts that are a known consequence of a root cause that's already paging someone.
Trigger: Manual — created ahead of time via the UI or
amtool.Goal: Planned noise suppression (a maintenance window, a deploy) that isn't tied to any other alert's state.
7. Complete alertmanager.yml
This ties every piece above into one file — global defaults, the routing tree, receivers, and inhibition rules together. The receivers block wires up three different notification channels to the same AlertManager instance:
pagerduty_configs — sends to PagerDuty's Events API v2 via a routing_key, with a templated description and structured details (firing count, cluster). Wired to severity=critical below — the receiver most likely to page someone.
slack_configs — posts to an incoming webhook api_url, targeting a channel, with a templated title and text built from .Alerts. Wired to severity=warning below — visible, but not a page.
email_configs — sends to a fixed to address using the smtp_* settings under global. Used as the default-email fallback receiver for anything that doesn't match a more specific route.
global:
resolve_timeout: 5m
smtp_smarthost: 'smtp.example.com:587'
smtp_from: 'alertmanager@example.com'
smtp_auth_username: 'alertmanager'
smtp_auth_password: 'secret'
pagerduty_url: 'https://events.pagerduty.com/v2/enqueue'
templates:
- '/etc/alertmanager/templates/*.tmpl'
route:
receiver: default-email
group_by: [alertname, cluster, namespace]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- matchers:
- severity = critical
receiver: pagerduty-critical
group_wait: 10s
repeat_interval: 1h
continue: false
- matchers:
- severity = warning
receiver: slack-warning
group_wait: 1m
repeat_interval: 8h
continue: false
- matchers:
- alertname = Watchdog
receiver: null-receiver
receivers:
- name: null-receiver
- name: pagerduty-critical
pagerduty_configs:
- routing_key: '<PAGERDUTY_INTEGRATION_KEY>'
description: '{{ template "pagerduty.default.description" . }}'
severity: critical
details:
firing: '{{ .Alerts.Firing | len }}'
cluster: '{{ .CommonLabels.cluster }}'
- name: slack-warning
slack_configs:
- api_url: 'https://hooks.slack.com/services/XXX/YYY/ZZZ'
channel: '#alerts-warning'
title: '[{{ .Status | toUpper }}] {{ .CommonLabels.alertname }}'
text: >-
{{ range .Alerts }}
*Alert:* {{ .Annotations.summary }}
*Severity:* {{ .Labels.severity }}
*Details:* {{ range .Labels.SortedPairs }} {{ .Name }}={{ .Value }} {{ end }}
{{ end }}
send_resolved: true
- name: default-email
email_configs:
- to: 'oncall@example.com'
send_resolved: true
inhibit_rules:
- source_matchers:
- severity = critical
target_matchers:
- severity = warning
equal: [alertname, cluster, namespace]
- source_matchers:
- alertname = NodeDown
target_matchers:
- alertname =~ "Pod.*"
equal: [node]
8. Debugging
Config validation:
amtool check-config /etc/alertmanager/alertmanager.yml
Query active alerts:
# All firing alerts
amtool alert query
# Filter by label
amtool alert query severity=critical
# Against a specific AlertManager
amtool alert query --alertmanager.url=http://alertmanager:9093
API endpoints:
# List all alerts (v2 API)
curl http://alertmanager:9093/api/v2/alerts | jq .
# List active silences
curl http://alertmanager:9093/api/v2/silences | jq .
# AlertManager status
curl http://alertmanager:9093/api/v2/status | jq .
# Reload config (SIGHUP or POST)
curl -X POST http://alertmanager:9093/-/reload
Common issues:
| Problem | Check |
|---|---|
| Alerts not routing | amtool config routes test severity=critical |
| Silence not working | Verify matcher syntax with amtool silence query |
| No notifications sent | Check amtool alert query — alert must be in AM first |
| Config errors on reload | amtool check-config before applying |