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.

0/0 checks

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
1. Inactive. The rule's condition is false — there's nothing to track yet.
2. Pending. The condition just went true. Prometheus starts counting against the rule's for: duration, but AlertManager hasn't heard anything yet.
3. Firing. The 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.
4. Resolved. The condition clears. A resolve notification goes out for the same alert AlertManager already dispatched, closing the loop for whoever got paged.

A rule's condition goes true, then false again 20 seconds later, and the rule has for: 2m. Does AlertManager ever see this alert?

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]
1. Start at the root route. Every alert enters here first. The root route's own receiver is the fallback if nothing more specific ever matches.
2. Check the first child route. Is 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.
3. Fall through if it didn't match. If team=infra didn't match, the tree moves to the next sibling route: severity=warning? A match here sends it to slack-warning.
4. Default receiver. If nothing along the way matched, the alert falls back to the root route's own receiver.

The routing tree checks team=infra? before severity=warning?. An alert arrives with both team=infra and severity=warning set. Which receiver gets it?

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)
1. First alert in a new group. AlertManager doesn't notify immediately — it starts the group_wait clock (typically 30s), giving related alerts a chance to land in the same batch.
2. group_wait elapses. The first notification goes out, containing every alert that arrived in that window, batched under the labels in group_by.
3. A new alert joins the group. AlertManager doesn't fire off a fresh notification right away — it waits group_interval (typically 5m) before sending an updated notification that includes the new alert.
4. Still firing, nothing new. If the group is still active after 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?

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?

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?

Grouping, inhibition, and silencing all reduce notification noise, but they solve different problems — worth keeping straight:

What: Batches multiple firing alerts that share 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.
What: Suppresses target alerts entirely while a source alert is firing (e.g. 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.
What: Mutes any alert matching a set of matchers for a fixed time window.
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