The most surprising thing about Prometheus alert fatigue is that the problem isn’t usually with the alerts themselves, but with how they’re grouped and silenced.

Let’s see how this plays out in the wild. Imagine you’ve got a basic node_exporter running and you’ve set up some alerts. One of the most common is for high CPU usage.

ALERT HighCpuUsage
  IF node_cpu_seconds_total{mode="idle"} == 0
  FOR 5m
  LABELS { severity = "warning" }
  ANNOTATIONS {

    summary = "High CPU usage detected on {{ $labels.instance }}",


    description = "CPU usage on {{ $labels.instance }} has been above 95% for the last 5 minutes. This may indicate a performance issue or runaway process.",

  }

This alert fires when the idle CPU mode counter has zero increments for 5 minutes, meaning the CPU has been 100% busy. Great, right? But what if you have 100 servers? Suddenly, you’re getting 100 alerts for high CPU. If this happens during a brief, system-wide blip (like a noisy cron job that runs every hour), you’re drowning.

The core issue is that alerts are often treated as individual events, rather than symptoms of a larger problem that can be batched. Prometheus’s Alertmanager is designed to fix this with grouping and silencing, but it’s often misconfigured or underutilized.

Here’s the mental model:

  1. Alert Generation: Prometheus scrapes metrics and evaluates alerting rules. If a rule’s condition is met for the specified FOR duration, an alert is sent to Alertmanager.
  2. Grouping: Alertmanager receives alerts and groups them based on shared labels. This is crucial. If multiple instances of the same type of alert fire (e.g., 10 servers all have high CPU), Alertmanager can bundle them into a single notification.
  3. Inhibition: If a specific alert is firing, it can inhibit other alerts. For example, if a ClusterDown alert is firing, you probably don’t need 50 individual PodNotReady alerts from that cluster.
  4. Silencing: This is for planned maintenance or known issues. You can temporarily silence alerts matching specific label sets.
  5. Routing: Alertmanager routes the grouped, inhibited, and un-silenced alerts to various receivers (Slack, PagerDuty, email).

The key levers you control are the alerting rules themselves, and critically, the group_by and group_wait/group_interval settings in Alertmanager’s configuration.

Let’s dive into Alertmanager’s configuration (alertmanager.yml):

global:
  resolve_timeout: 5m

route:
  group_by: ['alertname', 'cluster', 'service'] # Key for grouping!
  group_wait: 30s
  group_interval: 5m
  receiver: 'default-receiver'

  routes:
  - receiver: 'critical-alerts'
    matchers:
      severity: 'critical'
    group_by: ['alertname', 'cluster'] # Can override grouping per route
    continue: true # Allows alerts to be matched by subsequent routes

receivers:
- name: 'default-receiver'
  slack_configs:
  - channel: '#alerts-general'
    send_resolved: true

- name: 'critical-alerts'
  pagerduty_configs:
  - service_key: '...'

The group_by field is your primary weapon. By default, Alertmanager might group by just alertname. This means all HighCpuUsage alerts from different servers would be bundled if they have identical alertname and all other labels. This is rarely what you want. Instead, you want to group by labels that represent the context of the problem.

Consider group_by: ['alertname', 'cluster', 'service']. If HighCpuUsage fires on two different servers, but those servers belong to different cluster or service labels, they’ll be separate notifications. If they belong to the same cluster and service, they’ll be bundled. This is much more actionable.

group_wait is how long Alertmanager waits for more alerts to arrive before sending a notification for a new group. A longer group_wait (e.g., 1m or 2m) allows more related alerts to coalesce into a single notification. group_interval is how long it waits before sending a notification for a previously sent group that has new alerts.

The real magic often lies in how you define your alert labels. If your node_exporter instances are consistently labeled with cluster and service (e.g., cluster="prod-us-east-1", service="web-frontend"), Alertmanager can intelligently group alerts for the same service within the same cluster. Without these common labels, grouping by alertname alone will likely result in individual alerts.

The one thing most people don’t realize is that Alertmanager doesn’t just group alerts that are identical in their labels; it groups alerts that share the specified subset of labels in group_by. If an alert has labels {'alertname': 'HighCpuUsage', 'instance': 'server-1', 'cluster': 'prod'} and another has {'alertname': 'HighCpuUsage', 'instance': 'server-2', 'cluster': 'prod'}, and your group_by is ['alertname', 'cluster'], these two alerts will be bundled into a single notification, even though their instance labels differ. The instance label is effectively "consumed" by the grouping.

The next concept you’ll grapple with is how to effectively use inhibit_rules to suppress noisy, secondary alerts when a primary, more impactful alert is already firing.

Want structured learning?

Take the full Observability & Monitoring course →