Alertmanager routing tree examples: matchers, nesting, grouping, and continue
The route block is where most Alertmanager confusion lives. Working YAML examples for routing by severity, team, and environment - plus what group_wait, group_interval, and repeat_interval actually do, and how to test the tree with amtool before it decides who gets woken up.
The AlertKick team
Most Alertmanager problems are not delivery problems. The webhook works, Slack is connected, the alert is firing in Prometheus - and it still went to the wrong channel, or arrived forty times, or never arrived at all. That is nearly always the routing tree.
This post is the deep version of the route block. If you need to get Alertmanager installed and talking to Prometheus first, start with the Alertmanager setup guide and come back here when the basic config is running.
How matching actually works
The route block is a tree. The top-level route is the root, and it must have a receiver - that is the fallback that catches anything the children do not.
Alertmanager walks the tree depth-first. At each level it checks child routes in order and takes the first one whose matchers all pass. If that child has children of its own, it repeats the process one level down. If no child matches, the alert is delivered by the current node’s receiver.
Two rules cause most surprises:
- First match wins among siblings. Order matters. A broad matcher placed above a narrow one will swallow everything below it.
- A matched child ends the search at that level unless it sets
continue: true.
That second one is the escape hatch for “I want this alert to go to two places”, covered further down.
Matchers: the four operators
matchers is the current syntax. The older match (exact) and match_re (regex) fields are deprecated and should be migrated - one list now handles both:
matchers:
- severity = "critical" # equals
- environment != "dev" # not equals
- service =~ "api|web|worker" # regex matches (fully anchored)
- team !~ "test-.*" # regex does not match
Three things worth knowing:
- Regexes are fully anchored.
service =~ "api"matches the label valueapiand nothing else. If you want substring behaviour, writeservice =~ ".*api.*". - All matchers in a list must pass. It is AND, never OR. To express OR, use a regex or two sibling routes.
- A missing label is an empty string.
matchers: [team = ""]is a legitimate way to catch alerts that forgot to setteam, which is a genuinely useful safety net.
Example 1: route by severity
The most common starting shape. Criticals page, warnings go to chat, info is recorded but never announced.
route:
receiver: team-slack # the catch-all, always required
group_by: [alertname, cluster]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- matchers: [severity = "critical"]
receiver: oncall-pager
group_wait: 10s # criticals wait less before the first page
repeat_interval: 1h # and nag harder while unresolved
- matchers: [severity = "warning"]
receiver: team-slack
- matchers: [severity = "info"]
receiver: "null" # deliberately swallowed
The "null" receiver is a real receiver with no configs in it:
receivers:
- name: "null"
Being explicit about what you are discarding is better than leaving it to the catch-all, because six months later the config still says out loud that info alerts are intentionally silent.
Example 2: route by team, then by severity
Nesting is where the tree earns its name. Route to the owning team first, then decide urgency inside that team’s subtree - so each team controls its own escalation without touching anyone else’s.
route:
receiver: fallback-slack
group_by: [alertname, cluster, service]
routes:
- matchers: [team = "platform"]
receiver: platform-slack # platform's default
routes:
- matchers: [severity = "critical"]
receiver: platform-pager
- matchers: [team = "data"]
receiver: data-slack
routes:
- matchers: [severity = "critical"]
receiver: data-pager
Note what the child routes inherit. group_by, the timers, and anything else not overridden come from the parent, so you only write what differs. A platform alert with severity: warning matches the team = "platform" route, finds no matching child, and is delivered by platform-slack.
The fallback-slack receiver at the root is not decoration. Any alert with a team label you did not anticipate lands there, which is how you find out a new service is emitting alerts nobody owns.
Example 3: silence an environment without silencing the rules
Staging alerts firing into the production channel is the fastest way to train a team to ignore the channel. Route on environment above everything else:
route:
receiver: prod-slack
group_by: [alertname, cluster]
routes:
- matchers: [environment =~ "dev|staging"]
receiver: nonprod-slack
repeat_interval: 24h # visible, but not nagging
routes:
- matchers: [severity = "critical"]
receiver: nonprod-slack # still not a page, on purpose
Put this route first. Because first match wins, an environment = staging alert with severity = critical hits the non-prod subtree and never reaches the production paging routes below it.
Example 4: continue, for when one alert needs two destinations
By default a matched route ends the search. continue: true tells Alertmanager to keep evaluating the remaining siblings after this one matches, so a single alert can reach several receivers.
The usual case is an audit or ticketing sink that should see everything without interfering with normal routing:
route:
receiver: team-slack
routes:
- matchers: [severity =~ "critical|warning"]
receiver: incident-webhook
continue: true # <- keep going, do not stop here
- matchers: [severity = "critical"]
receiver: oncall-pager
- matchers: [severity = "warning"]
receiver: team-slack
Without continue: true, the first route would capture every critical and warning, and the pager route below it would be dead code that never fires. This is a genuinely common way to break a routing tree, and it fails silently - nothing errors, the pages just stop.
Grouping: the three timers, in plain terms
group_by decides what counts as “the same problem”. The three timers decide when you hear about it.
group_by: [alertname, cluster]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
group_wait(default 30s) - a new group has appeared. Wait this long before the first notification, so the other forty servers with the same problem land in the same message. Too short and you get one ping per host; too long and you delay the page.group_interval(default 5m) - a group you have already notified about has gained new alerts. Wait this long before sending an update. This is the anti-chatter timer.repeat_interval(default 4h) - nothing changed, the group is still firing. Re-send this often so an unresolved problem does not quietly fall out of memory.
Two special values of group_by are worth knowing:
group_by: ['...'] # do not group at all - one notification per alert
group_by: [] # group everything into a single group
['...'] (a literal three-dot string) is the escape hatch when you have a downstream system that wants one event per alert, such as a ticketing webhook. Do not use it for a human channel - that is how storms happen.
The most common grouping mistake is putting a high-cardinality label in group_by. Adding instance means a 40-server disk problem produces 40 groups and therefore 40 notifications, which is exactly the storm grouping exists to prevent. Group by what identifies the problem (alertname, cluster, service), not what identifies the machine.
Test the tree before it wakes anyone
amtool will tell you exactly where a set of labels ends up, and sends nothing while doing it:
# Print the whole tree as Alertmanager understands it
amtool config routes show
# Where would this specific alert land?
amtool config routes test \
--config.file=/etc/alertmanager/alertmanager.yml \
severity=critical team=platform environment=production
# Catch typos and bad matchers before a reload
amtool check-config /etc/alertmanager/alertmanager.yml
Run routes test against the four or five label combinations you actually care about and put them in a shell script. It takes ten minutes, it is the only way to verify continue behaves as you think, and it turns “I believe criticals page the on-call” into something you have checked.
The failure modes worth knowing
- Broad route above narrow route. The narrow one becomes unreachable.
routes showmakes this obvious. - Missing
continue: trueon a catch-all sink. Everything downstream silently stops. instanceingroup_by. Grouping stops working, storms return.- No root receiver that a human reads. Unowned alerts vanish into a channel nobody has open.
- Regex assumed to be a substring match.
service =~ "api"does not matchapi-gateway. - Editing the config without reloading.
amtool check-configvalidates the file, but Alertmanager still needssystemctl reload alertmanageror aPOST /-/reload.
Where this stops being worth it
A well-built routing tree is genuinely good engineering, and the config above will serve a small team for years. What it will not do is get someone out of bed. Alertmanager delivers a payload to a receiver and considers the job done - it has no concept of an on-call rotation, no acknowledgement, and no escalation to a second person when the first does not respond. That is the gap every team eventually hits, and it is why a webhook receiver named oncall-pager usually points at something else.
AlertKick fills that side: on-call rotations with overrides, escalation chains that keep climbing until somebody acknowledges, and notification channels ordered least to most disruptive. Point an Alertmanager webhook receiver at it and keep the routing tree you have already tuned, or skip the stack entirely and let the agent do the detection too.
Either way: build the tree, then test it with amtool before you trust it.
Frequently asked questions
- What is the difference between group_wait, group_interval, and repeat_interval in Alertmanager?
- group_wait (default 30s) is how long Alertmanager holds the first notification for a new group, so related alerts arrive together. group_interval (default 5m) is how long it waits before notifying again about a group that has gained new alerts. repeat_interval (default 4h) is how often it re-sends a notification for a group that has not changed but is still firing.
- Does Alertmanager routing stop at the first matching route?
- Within a set of sibling routes, yes - Alertmanager walks them in order and stops at the first match unless that route sets `continue: true`. Matching is depth-first, so a matching child route wins over its parent, and a route with no matching children delivers to its own receiver.
- Should I use matchers or match in alertmanager.yml?
- Use `matchers`. The older `match` and `match_re` fields are deprecated. `matchers` is a single list using the operators =, !=, =~, and !~, so `matchers: [severity =~ "critical|page"]` replaces what used to need match_re.
- How do I test an Alertmanager routing tree without sending real alerts?
- Use `amtool config routes test`, which takes a set of labels and prints which receiver they would reach. `amtool config routes show` prints the whole tree. Both read your live config and send nothing, so they are safe to run against production.