Where to start First steps track · 15 stages
Linux Labs

Alertmanager: who gets alerts and how often

18 min Ubuntu 24.04 · Alertmanager 0.26

The service that takes alerts from Prometheus, groups and silences them, and routes notifications to receivers.

Cheat sheet

9 commands Printable version
  • amtool check-config /etc/prometheus/alertmanager.yml validate
  • sudo systemctl reload prometheus-alertmanager reread the config
  • amtool alert active alerts
  • amtool alert query severity=critical filter by labels
  • amtool alert query -o extended all labels
  • amtool silence add instance=web01 -d 2h -c 'disk swap' silence
  • amtool silence query active silences
  • amtool silence expire 6c91da38-1b58-4c6f-835e-3b504d18dd7b lift
  • amtool alert add alertname=Test severity=warning a test alert

Overview

Alertmanager is the service that receives alerts from Prometheus and decides whom to tell, when and how many times. Prometheus decides by a rule that an alert is firing (prometheus) and sends it to Alertmanager. From there Alertmanager:

  1. groups alerts by labels, so one event gives one notification rather than one per machine;
  2. picks a receiver through the routing tree;
  3. skips alerts that are silenced or inhibited by another alert;
  4. sends the notification (an email, a webhook, a chat message) and repeats it while the alert keeps firing.
text
Prometheus ──alerts──▶ Alertmanager :9093 ──notifications──▶ receivers
 (rules)               groups, routes,                    (webhook,
                       silences, inhibition                email…)

In Ubuntu this is the prometheus-alertmanager package: a service of the same name, the config /etc/prometheus/alertmanager.yml, and data (silences and the log of what was sent) in /var/lib/prometheus/alertmanager. Port 9093 is the web UI and the API, 9094 links cluster nodes. The amtool utility comes in the same package. On the training machine the package is installed and the service is off, and the packaged config is an example with email and PagerDuty: with no network it is useless, so you write your own.

The path of an alert

Where Prometheus sends alerts is the alerting section of prometheus.yml; the package sets localhost:9093. Whether Prometheus has found Alertmanager:

text
$ curl -s localhost:9090/api/v1/alertmanagers | jq -c .data
{"activeAlertmanagers":[{"url":"http://localhost:9093/api/v2/alerts"}],
  "droppedAlertmanagers":[]}

Prometheus builds the sending address from scheme (http by default), the address from targets, path_prefix (empty by default) and /api/v2/alerts; the full address is the url in the answer above. An extra path_prefix, such as /alertmanager copied from a server behind a proxy, gives an address Alertmanager does not have (alertmanager).

Prometheus sends only alerts in the firing state and keeps resending them while they fire; once the condition no longer holds, it sends the alert once more with an end time, and Alertmanager marks it resolved. Prometheus adds the external_labels from its config to the alert's labels: the packaged config has monitor: 'example', so every alert carries monitor="example".

An alert in Alertmanager is labels (they tell alerts apart, group them and pick the route), annotations (text: summary, description) and a start and end time.

Grouping and intervals

group_by lists labels: alerts with the same values of these labels fall into one group, and a group is one notification with a list of alerts. On the training machine, three InstanceDown alerts for web01, web02 and web03:

group_by Notifications In each
[alertname] 1 three alerts
[alertname, instance] 3 one alert

Without group_by all alerts of the route are one group; group_by: ['...'] puts every alert on its own.

Setting What it sets Default
group_wait the pause before a new group's first notification, to collect the alerts that follow 30s
group_interval the pause before a notification about a change in the group: a new alert in it or a resolved one 5m
repeat_interval the repeat when nothing in the group has changed 4h

Fifty notifications for one event almost always come from grouping: group_by has a label that is unique to each alert (instance, pod, path), or '...'. The same notification every few minutes means repeat_interval is too short.

Routes

Routes form a tree. The root route takes every alert and must have a receiver. Child routes under routes are checked top to bottom; the alert goes to the first one whose matchers fit and goes deeper if that one has routes of its own. When none fits, the parent's receiver gets the notification. continue: true on a route means "check the following ones too": that is how one notification reaches several receivers. A child route inherits the parent's group_by and intervals unless it sets its own; a group_by it sets replaces the parent's entirely, the labels do not add up.

Critical alerts to both the on-call person and the team:

yaml
route:
  receiver: team
  routes:
    - matchers: [severity="critical"]
      receiver: oncall
      continue: true
    - matchers: [team="web"]
      receiver: team-web

Without continue: true, a critical alert of the web team would go to the on-call person only: checking would stop at the first route that matched.

Conditions in matchers look like promql selectors: severity="critical", team!="db", severity=~"critical|page". Every condition in the list has to hold. The old match and match_re forms are still read.

yaml
route:
  receiver: chat
  group_by: [alertname]
  routes:
    - matchers:
        - severity="critical"
      receiver: pager

amtool draws the tree and tells where an alert with given labels will go (the --config.file flag is required, see alertmanager):

text
$ amtool config routes show \
    --config.file=/etc/prometheus/alertmanager.yml
Routing tree:
.
└── default-route  receiver: chat
    └── {severity="critical"}  receiver: pager

$ amtool config routes test \
    --config.file=/etc/prometheus/alertmanager.yml severity=critical
pager

Silences and inhibition

A silence is a temporary ban on notifications for alerts whose labels match its conditions. A person sets it, with amtool silence add or the web UI, with a comment and a duration. A silenced alert stays in the list with the suppressed state, and no notifications go out for it. Silences are stored in /var/lib/prometheus/alertmanager and survive a restart.

Inhibition is a rule in the config: while a source alert fires, target alerts with the same values of the equal labels are not sent. When a machine is down, there is no point in a separate message that its disk is filling up:

yaml
inhibit_rules:
  - source_matchers: [alertname="NodeDown"]
    target_matchers: [severity="warning"]
    equal: [instance]

On the training machine: a silence on alertname=DiskFull instance=web02 means no notification about web02 while web04 still gets one; NodeDown on web03 puts DiskFull for the same web03 into the suppressed state.

amtool

amtool is Alertmanager from the terminal: it lists alerts, sets and lifts silences, checks the config and the routes. The alert and silence commands go to the Alertmanager API. On the training machine amtool goes to http://localhost:9093 by default; another address is --alertmanager.url=… or a line in /etc/amtool/config.yml (for everyone) or ~/.config/amtool/config.yml (for you):

yaml
alertmanager.url: http://localhost:9093

Alerts:

text
$ amtool alert
Alertname  Starts At                Summary      State
DiskFull   2026-10-22 04:44:53 UTC  disk is low  active
DiskFull   2026-10-22 04:44:53 UTC               active

Summary is the summary annotation. Silenced and inhibited alerts are not in the amtool alert output; -s (--silenced) and -i (--inhibited) show them. amtool alert query instance=web01 filters by labels, -o extended prints every label and annotation, -s shows silenced alerts, -i inhibited ones, -o json gives everything as JSON for jq.

Silences:

text
$ amtool silence add alertname=DiskFull instance=web01 \
    -d 30m -c 'disk swap'
6c91da38-1b58-4c6f-835e-3b504d18dd7b
$ amtool silence query
ID                                    Matchers                       …
6c91da38-1b58-4c6f-835e-3b504d18dd7b  alertname="DiskFull" instance=…

The silence query output is cut here: the Ends At, Created By and Comment columns follow. The command prints the new silence's ID; amtool silence expire <ID> lifts it, and amtool silence query -q lists the IDs of all silences. -d is the duration (30m, 2h), -c the comment; without one the silence is not set.

A test alert without Prometheus, to check the route and the receiver:

bash
amtool alert add alertname=Test severity=warning \
  --annotation=summary='delivery check'

Configuration

14

alertmanager.yml

A config with two webhook receivers: everything goes to chat, critical alerts to pager.

yaml
route:
  receiver: chat
  group_by: [alertname]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - matchers:
        - severity="critical"
      receiver: pager
receivers:
  - name: chat
    webhook_configs:
      - url: http://127.0.0.1:5001/chat
  - name: pager
    webhook_configs:
      - url: http://127.0.0.1:5001/pager

The webhook gets a POST with JSON: receiver, status (firing or resolved), groupLabels, commonLabels and the list of alerts with the labels, annotations and status of each. For a webhook, send_resolved is on by default: when an alert is resolved, a notification about that arrives too.

Check and apply:

bash
amtool check-config /etc/prometheus/alertmanager.yml
sudo systemctl reload prometheus-alertmanager
journalctl -u prometheus-alertmanager -o cat | tail -3

check-config prints SUCCESS and what it found: the route, the number of receivers and inhibit rules. After reload the journal says Completed loading of configuration file.

Startup flags and the cluster

The service flags are the ARGS variable in /etc/default/prometheus-alertmanager (empty in the package). By default Alertmanager starts as a cluster node: it listens on port 9094 and looks for the machine's private address to announce to its peers. On a machine without one (the training VM has only lo) the service does not start (alertmanager). A single node without a cluster:

bash
# /etc/default/prometheus-alertmanager
ARGS="--cluster.listen-address="

Flags are read at start: after an edit, run sudo systemctl restart prometheus-alertmanager. curl -s localhost:9093/api/v2/status | jq .cluster.status then says "disabled".

Directive What it does Example
route alertmanager.yml The root route every alert passes through; a receiver is required. route: {receiver: chat}
receiver route The name of a receiver from the receivers list that gets this route's notifications. receiver: pager
group_by route The labels alerts are grouped by; one group is one notification. '...' puts every alert on its own. group_by: [alertname]
group_wait route The pause before a new group's first notification, to collect the alerts that follow. group_wait: 30s
group_interval route The pause before a notification about a change in the group, a new or a resolved alert. group_interval: 5m
repeat_interval route How long before the notification repeats when nothing in the group has changed. repeat_interval: 4h
routes route Child routes; an alert goes to the first one that matches, otherwise it stays with the parent. routes: [{matchers: [severity="critical"], receiver: pager}]
matchers route, inhibit_rules Conditions on the alert labels, =, !=, =~ or !~; all of them have to hold. matchers: [severity="critical", team=~"web|db"]
continue route true keeps checking the following routes after this one, so several receivers get the notification. continue: true
receivers alertmanager.yml The list of receivers; each has a name and delivery methods, webhook_configs, email_configs and others. receivers: [{name: chat, webhook_configs: [{url: http://127.0.0.1:5001/chat}]}]
webhook_configs.url receivers The address Alertmanager POSTs the notification JSON to. url: http://127.0.0.1:5001/chat
send_resolved webhook_configs Whether to report that an alert is resolved; on by default for webhooks, off for email. send_resolved: false
inhibit_rules alertmanager.yml Inhibition; while a source alert fires, target alerts with the same equal labels are not sent. inhibit_rules: [{source_matchers: [alertname="NodeDown"], target_matchers: [severity="warning"], equal: [instance]}]
resolve_timeout global How long before an alert with no endsAt counts as resolved once it stops arriving. global: {resolve_timeout: 5m}

Nothing found. Try a shorter query.

Recipes

4

Check a route before sending

bash
c=/etc/prometheus/alertmanager.yml
amtool config routes test --config.file=$c severity=critical team=db
amtool config routes test --config.file=$c --verify.receivers=pager \
  severity=critical

The first command prints the receiver name. The second compares it with the expected one: on a mismatch it prints WARNING: Expected receivers did not match resolved receivers. and exits with code 1, which suits a check script.

Send a test alert through the API

bash
curl -s -XPOST -H 'Content-Type: application/json' \
  localhost:9093/api/v2/alerts \
  -d '[{"labels":{"alertname":"Test","severity":"warning"}}]'

The array can carry many alerts at once. An alert without endsAt counts as resolved after resolve_timeout (5 minutes) unless it is sent again.

A silence for maintenance

bash
amtool silence add instance=web01 -d 2h -c 'disk swap, Ivan'

The condition covers only the machine you are working on, the duration only the work. Done early: amtool silence expire <ID>.

Do not page for consequences

The rule from alertmanager: during NodeDown, warnings from the same machine stay quiet. Without equal, one machine being down would mute the warnings from all of them.

Troubleshooting

10

The service does not start: No private IP address found

text
$ journalctl -u prometheus-alertmanager -o cat | grep level=error
… level=error msg="unable to initialize gossip mesh" err="create
  memberlist: Failed to get final advertise address: No private IP
  address found, and explicit IP not provided"

Cluster mode found no private address on the machine. The service crashes and restarts until systemd gives up: Start request repeated too quickly. The fix is ARGS="--cluster.listen-address=" (alertmanager), then sudo systemctl reset-failed prometheus-alertmanager and start.

amtool: connection refused

text
amtool: error: Get "http://localhost:9093/api/v2/alerts?active=true…":
  dial tcp 127.0.0.1:9093: connect: connection refused

Alertmanager is not running or listens on another address: systemctl status prometheus-alertmanager, ss -tlnp | grep 9093.

comment required by config

amtool silence add without -c: a silence needs a comment. Add -c 'what and why'.

undefined receiver

text
$ amtool check-config /etc/prometheus/alertmanager.yml
Checking '/etc/prometheus/alertmanager.yml'  FAILED: undefined
  receiver "oncall" used in route
amtool: error: failed to validate 1 file(s)

A route refers to a receiver that is not in receivers, usually a typo in the name. If such a config is reloaded, Alertmanager keeps running with the old one, and the journal says Loading configuration file failed. A service that is active has not necessarily accepted the new config.

scheme required for webhook url

amtool config routes show or test without --config.file take the config of the running Alertmanager through the API. The API hides webhook addresses, and amtool cannot parse such a config. Pass the file: --config.file=/etc/prometheus/alertmanager.yml.

A notification does not arrive: Notify attempt failed

text
… level=warn component=dispatcher receiver=chat integration=webhook[0]
  msg="Notify attempt failed, will retry later" attempts=1
  err="Post \"<redacted>\": dial tcp 127.0.0.1:5001: connect:
  connection refused"

The receiver does not answer. Alertmanager retries by itself; fix the receiver and the notification goes out. The address is hidden in the journal, so look at url in the config.

The alert fires in Prometheus but is not in Alertmanager

  • the alert is still pending: Prometheus sends only firing ones (prometheus);
  • curl -s localhost:9090/api/v1/alertmanagers shows an empty activeAlertmanagers: prometheus.yml has no alerting section, or the address in it is wrong;
  • Alertmanager is not running: amtool alert answers connection refused.

Error sending alert: bad response status 404

text
$ journalctl -u prometheus -o cat | grep 'Error sending alert'
… msg="Error sending alert"
  alertmanager=http://localhost:9093/alertmanager/api/v2/alerts
  count=1 err="bad response status 404 Not Found"

Prometheus sends alerts, and Alertmanager answers 404: the address is built wrong (alertmanager). amtool alert is empty meanwhile, although ALERTS fires in Prometheus. Compare the url in curl -s localhost:9090/api/v1/alertmanagers with http://localhost:9093/api/v2/alerts and remove the extra path_prefix.

Too many notifications

Look at the labels the group was built from: groupLabels in the webhook body. If it has instance or another label unique to each alert, remove it from group_by. For repeats of the same thing, raise repeat_interval.

An alert in the suppressed state

A silence or an inhibit rule mutes it. Which one is in the JSON:

text
$ amtool alert query -o json alertname=DiskFull | jq -c '.[].status'
{"inhibitedBy":[],"silencedBy":["6c91da38-…"],"state":"suppressed"}

An ID in silencedBy is a silence from amtool silence query; one in inhibitedBy is the source alert of an inhibit rule.

Danger zone

1

A broad silence

amtool silence add severity=~".+" -d 7d mutes every alert for a week, and a day later nobody remembers it. Aim the condition at a specific machine or alert, keep the duration to the work, and say who and why in the comment. amtool silence query -q | xargs amtool silence expire lifts every silence at once, other people's too.

Terms

5
alert grouping
Putting alerts with the same values of the group_by labels into one group: one group, one notification. A label unique to each alert in group_by gives one notification per alert.
alert route
A node of the route tree in the Alertmanager config: matchers on the alert labels and a receiver. An alert goes to the first child route that matches, otherwise to the parent's receiver.
receiver
A named way Alertmanager delivers notifications: a webhook, email, a chat. A route names its receiver from the receivers list.
silence
A temporary ban on Alertmanager notifications for alerts with matching labels. Set with a comment and a duration, amtool silence add; a silenced alert is suppressed. Also appears in: shellcheck, ping, Output redirection and pipelines, sleep and 1 more
inhibition
An inhibit_rules rule in Alertmanager: while a source alert fires, target alerts with the same equal labels are not sent. For example, during NodeDown the same machine's warnings stay quiet.

See also

  • prometheus — the rules that produce alerts.
  • promql — rule expressions and the syntax of label conditions.
  • node-exporter — machine metrics that rules are written on.
  • grafana — Grafana's own alerts and notifications.
  • jq — parse amtool alert query -o json and the webhook body.
  • curl — the Alertmanager API from the terminal.
  • systemctl — reload, restart, reset-failed.
  • journalctl — the service journal: config and delivery errors.
Loading…