During a production incident, 200 alerts fire and 180 of them are noise from one root cause. This module teaches you how to group, deduplicate, inhibit, and correlate alerts so your team sees one clear signal instead of a storm
### What actually happens during a production incident It is 2 AM. Your payment service goes down. Within 90 seconds, 200 alerts fire across your monitoring stack. Your phone explodes. Slack is unreadable. Your on-call engineer is staring at a wall of red with no idea where to start. Here is the brutal truth: 180 of those 200 alerts are not real problems. They are symptoms. One database node went down, and every service that depends on it started screaming — every API timeout, every connection refused, every health check failure. All of it traces back to one root cause. But your engineer does not know that yet. They are drowning. This is an **alert storm**. And it destroys incident response time. Studies show that engineers spend 40% of their time on incident management. More than half of all alerts are not actionable. Over 80% of engineers admit they ignore alerts occasionally — not because they are lazy, but because the noise has trained them to. The result: real problems get buried. An outage that should take 10 minutes to fix takes 2 hours because the engineer cannot see the signal through the noise. ### Alert vs event vs incident These three words get used interchangeably in ops. They mean different things. An **event** is anything that happens in your system — a pod restarted, a CPU spiked, a disk write completed. Events are raw data. Your system generates thousands per minute. Most are completely normal. An **alert** is an event that crossed a threshold you care about. You told Prometheus: if CPU stays above 90% for 5 minutes, fire an alert. That alert is a signal that something might be wrong. Alerts are not confirmed problems — they are questions the system is asking you. An **incident** is a confirmed problem impacting users or services. One database going down is one incident. The 200 alerts it triggers are not 200 incidents. They are 200 signals pointing at the same incident. When you treat every alert as a separate incident, your team burns out. When you group alerts into incidents correctly, your team can focus. ### Root cause alerts vs symptom alerts During an alert storm, most alerts are symptoms. This is the most important mental model in incident response. Imagine an order service that depends on a Redis cache: ``` Redis cluster goes down ↓ Order service → "cannot connect to cache" alert ↓ API gateway → "order service returning 503" alert ↓ Mobile app → "API timeout" alert ``` Three alerts. One root cause. The Redis alert is the root cause alert. The other two are symptom alerts — they fired because Redis went down, not because anything is independently wrong with the order service or API gateway. If you page three different engineers for those three alerts, you have wasted two engineers' time. Fix Redis and all three alerts resolve automatically. The goal of alert correlation is to automatically identify this relationship and suppress the symptoms when the root cause is already being handled. ---
### Where it sits Alertmanager is a separate process from Prometheus. A lot of beginners think it is part of Prometheus — it is not. Here is the complete flow: ``` Your application (exposes /metrics) ↓ Prometheus (scrapes every 15s, evaluates alert rules) ↓ fires alert via HTTP POST Alertmanager (groups, deduplicates, inhibits, silences) ↓ Receivers (Slack, PagerDuty, email, webhook) ``` Prometheus evaluates your alerting rules. When a condition is true for the configured duration, it sends the alert to Alertmanager. Alertmanager then decides: should it notify someone? Should it group this with other alerts? Should it suppress it because a more critical alert is already firing? Alertmanager is stateful — it remembers which alerts have been sent, which are silenced, and which are inhibited. Prometheus is stateless about notifications — it just keeps sending the alert to Alertmanager until the condition resolves. > **Important:** Prometheus sends alerts continuously while the condition is true. Alertmanager uses `repeat_interval` to decide how often to re-notify. Without this, your on-call engineer gets paged every 15 seconds. ### What Alertmanager does and does not do **Does:** * Group multiple related alerts into one notification * Deduplicate the same alert from multiple Prometheus servers * Suppress downstream alerts when a root cause alert fires * Silence alerts during maintenance windows * Route different alerts to different teams or channels * Throttle notifications to prevent spam **Does not:** * Evaluate alert rules — that is Prometheus * Store metrics or time series data * Auto-remediate problems * Understand your service topology automatically Alertmanager works entirely on **labels**. Every alert carries labels — key-value pairs like `alertname="HighCPU"`, `severity="critical"`, `service="payments"`, `cluster="prod-mumbai"`. Alertmanager uses these labels to make every decision. This is why your Prometheus alert rules need good labels. Garbage labels in Prometheus means Alertmanager cannot do its job. ---
### What grouping does Grouping is Alertmanager's primary noise reduction mechanism. Instead of one notification per alert, it batches related alerts into a single notification. Without grouping: 50 pods lose database connectivity → 50 pages in 30 seconds. With grouping on `cluster` and `alertname`: 1 page saying "50 instances of DatabaseConnectionFailed in prod-mumbai cluster." That is the difference between a team that responds effectively and a team that is paralyzed. ### The four fields that control grouping **`group_by`** — which labels decide what belongs in the same group. ```yaml group_by: ['alertname', 'cluster', 'service'] ``` Alerts with the same values for all these labels are batched together. A `HighMemory` alert in `prod-mumbai` for `payments` groups with other `HighMemory` in `prod-mumbai` for `payments`. It does not group with `HighMemory` in `prod-delhi` or `payments` in a different cluster. **`group_wait`** — how long to wait before sending the first notification for a new group. ```yaml group_wait: 30s ``` During an incident, alerts do not all fire at exactly the same moment. A database going down triggers a chain of alerts over 20-30 seconds. A 30s wait collects them all before sending one combined notification instead of multiple incomplete ones. > **Common mistake:** Setting `group_wait` too low (5s) means your first notification goes out before all related alerts arrive — engineer sees an incomplete picture. Too high (5m) means waiting 5 minutes before anyone knows about the incident. 30s is a good default. **`group_interval`** — after the first notification, how long to wait before sending updates when new alerts join the group. ```yaml group_interval: 5m ``` **`repeat_interval`** — how long to wait before re-notifying about a group that has not changed. This prevents Alertmanager going silent on a persistent problem. ```yaml repeat_interval: 4h ``` Without `repeat_interval`, if your team does not resolve the alert, Alertmanager goes quiet after the first notification. With `repeat_interval: 4h`, they get reminded every 4 hours the problem is still open. ### A complete routing config with comments ```yaml global: # How long to wait before marking a resolved alert as "resolved" # if Prometheus stops sending it resolve_timeout: 5m route: # Default receiver if no child route matches — always set this # so no alert disappears silently receiver: 'default-slack' # Group alerts by alertname + cluster at the root level group_by: ['alertname', 'cluster'] # Wait 30s for more alerts before sending the first notification group_wait: 30s # After first notification, wait 5m before sending updates for new alerts group_interval: 5m # Re-notify every 4h if alert is still firing and nothing changed repeat_interval: 4h routes: # Critical payment alerts go to PagerDuty with faster timing - receiver: 'pagerduty-payments' matchers: - severity="critical" - service="payments" # Faster grouping for critical alerts — we want fast notification group_wait: 10s group_interval: 1m repeat_interval: 30m # Database alerts go to the DB team channel # service=~"mysql|redis|postgres" means: match any of these three values # =~ is a regex match — the | means "or" - receiver: 'slack-db-team' matchers: - service=~"mysql|redis|postgres" group_by: ['alertname', 'cluster', 'service'] group_wait: 30s # Warning alerts — less urgent, longer intervals - receiver: 'slack-warnings' matchers: - severity="warning" group_wait: 1m repeat_interval: 8h receivers: - name: 'default-slack' slack_configs: - api_url: 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL' channel: '#alerts' # {{ .GroupLabels.alertname }} is a Go template — it inserts the value # of the alertname label from the group into the message title: '{{ .GroupLabels.alertname }} - {{ .GroupLabels.cluster }}' # range .Alerts loops over all alerts in the group and prints each summary text: '{{ range .Alerts }}{{ .Annotations.summary }}{{ end }}' - name: 'pagerduty-payments' pagerduty_configs: - routing_key: 'YOUR_PAGERDUTY_KEY' - name: 'slack-db-team' slack_configs: - api_url: 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL' channel: '#db-alerts' - name: 'slack-warnings' slack_configs: - api_url: 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL' channel: '#warnings' ``` ---
### How it works automatically When you run multiple Prometheus servers for high availability — which every production setup should — the same alert can fire from multiple sources at once. Without deduplication, your on-call engineer gets the same page two or three times. Alertmanager solves this with **fingerprinting**. Every alert gets a fingerprint — a hash computed from its labels. Think of it like a unique ID generated from the alert's content: if two alerts have exactly the same labels, they produce the same fingerprint. When two alerts arrive with the same fingerprint, Alertmanager treats them as one alert regardless of which Prometheus server sent them. ``` Prometheus Server 1 → HighCPU{service="api", cluster="prod"} Prometheus Server 2 → HighCPU{service="api", cluster="prod"} ↓ Alertmanager fingerprints match → one notification sent ``` This works automatically. You do not configure it — it is built in. What you do need to ensure: both Prometheus servers use exactly the same labels. If one sends `cluster="prod"` and the other sends `cluster="production"`, fingerprints will not match and you get duplicate notifications. ### Repeat interval and severity The right `repeat_interval` depends on alert severity. A pattern that works well: | Severity | repeat_interval | |---------|----------------| | critical | 30m | | warning | 4h | | info | 24h | Configure this per route so each severity level gets appropriate repeat behavior. ---
### What silences are A silence is a temporary mute applied to specific alerts. While active, matching alerts are suppressed — they still fire and are tracked internally, but no notifications go out. Silences exist for one main use case: **planned work**. When your team is deploying a new version of the payments service, you expect some alerts during the deployment window. Create a silence, do your deployment, no one gets woken up unnecessarily. Silences are intentional and time-bounded. If you find yourself creating the same silence every week, that is a signal your alerting threshold is wrong — not that you need a recurring silence. ### Creating silences with amtool Alertmanager ships with a CLI tool called `amtool` and a web UI at port 9093. ```bash # Silence all alerts for the payments service for 2 hours # --author tracks who created this silence # --comment explains why it exists amtool silence add \ --alertmanager.url=http://localhost:9093 \ --duration=2h \ --author="priya@company.com" \ --comment="Deploying payments service v2.4.1" \ service="payments" # List active silences amtool silence --alertmanager.url=http://localhost:9093 # Expire a silence early when deployment finishes ahead of schedule amtool silence expire --alertmanager.url=http://localhost:9093 <silence-id> ``` ### Recurring maintenance windows in config For recurring windows — like a weekly database backup that always causes high I/O alerts Sunday at 3 AM: ```yaml # Define the recurring time window time_intervals: - name: weekly-maintenance time_intervals: - times: - start_time: '02:00' end_time: '04:00' weekdays: ['sunday'] # Always set location — without it Alertmanager uses UTC # 3 AM IST is actually 9:30 PM UTC the previous day location: 'Asia/Kolkata' route: receiver: 'default-slack' routes: - receiver: 'slack-db-team' matchers: - service=~"mysql|redis" # This route goes silent during the weekly maintenance window mute_time_intervals: - weekly-maintenance ``` ---
### Silencing vs inhibition — which to use when Both suppress alerts but solve different problems. **Silencing** is manual and time-based. A human says: "I know work is happening, mute these alerts for 2 hours." **Inhibition** is automatic and condition-based. You define a rule: "If alert A is firing, automatically suppress alert B." It triggers based on the presence of other alerts, not time. Use silencing for planned maintenance. Use inhibition for known dependency relationships — to suppress symptom alerts when the root cause alert is already firing. The rule: if you are suppressing because of human action, use a silence. If you are suppressing because of a system dependency, use inhibition. ### How inhibition rules work An inhibition rule has three parts: * `source_matchers` — the alert that does the suppressing (root cause) * `target_matchers` — the alert being suppressed (symptom) * `equal` — labels that must have the same value in both alerts The `equal` field is critical. Without it, a critical database alert in `prod-mumbai` would suppress warning alerts in `prod-delhi` — which makes no sense. The `equal` field scopes inhibition to the right logical boundary. How it works step by step: ``` 1. DatabaseDown{cluster="prod-mumbai", severity="critical"} fires 2. Alertmanager checks inhibition rules 3. Rule: if source=critical, suppress target=warning 4. equal: ['cluster'] — only suppress if cluster matches 5. All warning alerts in prod-mumbai are suppressed 6. Warning alerts in prod-delhi are NOT suppressed ``` ### Inhibition config for a real ops scenario Scenario: An order platform's matching engine depends on Redis and PostgreSQL. When the database goes down during peak hours, dozens of alerts fire across services. Without inhibition: 40 pages. With inhibition: 1. ```yaml inhibit_rules: # Rule 1: Critical alert suppresses warnings in the same cluster # Use case: DB down (critical) suppresses "high latency" warnings downstream - name: "suppress-warnings-when-critical" source_matchers: - severity="critical" target_matchers: - severity="warning" # Only suppress within the same cluster # Critical in prod-mumbai should NOT silence warnings in prod-delhi equal: ['cluster'] # Rule 2: Database down suppresses all dependent service alerts # Use case: PostgreSQL down should suppress order-service, auth-service alerts # because those alerts exist ONLY because the DB is down - name: "suppress-downstream-when-db-down" source_matchers: - alertname="PostgreSQLDown" - severity="critical" target_matchers: - alertname=~"OrderServiceUnavailable|AuthServiceTimeout|APIHighLatency" # Scope to same cluster AND environment # prevents staging inhibitions affecting production equal: ['cluster', 'environment'] # Rule 3: Warning suppresses info in same service # Use case: Reduce total notification volume during degraded state - name: "suppress-info-when-warning" source_matchers: - severity="warning" target_matchers: - severity="info" equal: ['cluster', 'service'] ``` > **Most common mistake:** Writing inhibition rules without the `equal` field, or with too few labels in it. A broad inhibition rule can suppress alerts in completely unrelated clusters or services. Always ask: "What scope should this suppression apply to?" then add those labels to `equal`. ---
What actually happens during a production incident It is 2 AM. Your payment service goes down. Within 90 seconds, 200 al...
Where it sits Alertmanager is a separate process from Prometheus. A lot of beginners think it is part of Prometheus — it...
What grouping does Grouping is Alertmanager's primary noise reduction mechanism. Instead of one notification per alert, ...
How it works automatically When you run multiple Prometheus servers for high availability — which every production setup...
What silences are A silence is a temporary mute applied to specific alerts. While active, matching alerts are suppressed...
Silencing vs inhibition — which to use when Both suppress alerts but solve different problems. Silencing is manual and t...
Alertmanager handles rule-based correlation well. But real AIOps systems use three different correlation approaches depe...
What you will build Alertmanager running locally with grouping and inhibition configured. You will fire test alerts thro...
Correlation is the input to everything else Event correlation is not just about reducing notification noise. It is the f...
Config fields Field Where What it controls groupby route Which labels define a group groupwait route Wait before first n...
Aligns directly with DevOps, Site Reliability (SRE), and Platform Engineering job descriptions.