Overview
Alerting is the “last mile” of a monitoring system — and the hardest to get right. A common predicament: servers run dozens of alerting rules generating hundreds of alert notifications daily. On-call engineers, bombarded by WeChat/DingTalk/email, gradually become desensitized — truly urgent alerts are drowned in noise until customer complaints reveal the system has been broken for hours.
The golden rule of SRE: every alert must have a clear action. If an alert doesn’t require immediate action or tracking, it shouldn’t exist. This article starts from alert fatigue and systematically covers alert tiering, SLO-based alert design, inhibition and aggregation strategies, alert metrics, and governance methods — helping you extract true “signal” from “alert noise.”
Reference: Google SRE Book “Monitoring Distributed Systems”, Prometheus Alerting Best Practices
I. Alert Fatigue: The Root Cause
1.1 Typical Symptoms of Alert Flooding
Team alert statistics (one week):
┌──────────────────────────┬────────┬──────────┐
│ Alert Type │ Count │ Acted On │
├──────────────────────────┼────────┼──────────┤
│ CPU usage > 80% │ 156 │ 3 │
│ Disk usage > 70% │ 89 │ 2 │
│ Pod restart │ 34 │ 5 │
│ HTTP 5xx error rate > 1% │ 12 │ 4 │
│ DB connections > 80% │ 8 │ 1 │
│ Certificate expiring │ 3 │ 1 │
│ Service unreachable │ 2 │ 2 │
├──────────────────────────┼────────┼──────────┤
│ Total │ 304 │ 18 │
└──────────────────────────┴────────┴──────────┘
Effective alert rate: 18/304 = 5.9%
Less than 6% of alerts require actual action; the remaining 94% is noise. In this state, on-call engineer behavior patterns become:
- See alert notification → glance at it → judge “same old problem” → ignore
- When a truly urgent alert arrives → also ignored → incident escalates
- Post-mortem: “There were too many alerts, didn’t notice the critical one”
1.2 Common Causes of Alert Flooding
| Cause | Symptom | Root Cause |
|---|---|---|
| Unreasonable thresholds | CPU > 80% alerts frequently | Threshold too low, normal load triggers it |
| No alert tiering | All alerts go to the same channel | No severity differentiation |
| Missing inhibition | Upstream failure triggers downstream cascade | No inhibit_rules configured |
| Duplicate alerts | Same issue notified every hour | Improper repeat_interval |
| No alert documentation | Don’t know how to handle received alert | Missing runbook link |
| Auto-resolving alerts | Pod restarts and immediately recovers | Resolution notifications also go to channel |
1.3 Signal vs. Noise
Alert quality = Signal / Total alerts
Signal: Issues requiring human intervention or attention
Noise: Auto-resolving, duplicate, false-positive, meaningless alerts
Target: Signal ratio > 80%, noise ratio < 20%
SRE Principle: If an alert can’t point to a specific action (fix, scale, record, investigate), it shouldn’t exist. Better to have fewer alerts than alert fatigue.
II. Alert Tiering Strategy
2.1 Four-Level Alert System
| Level | Label | Notification Method | Response Time | Example |
|---|---|---|---|---|
| P0 - Critical | severity=critical | Phone + SMS + IM | < 5 min | Core service down, data loss |
| P1 - Warning | severity=warning | IM + Email | < 30 min | Partial feature degradation, error rate increase |
| P2 - Notice | severity=info | Email / Daily report | Business hours | Disk 70%, certificate 30 days to expiry |
| P3 - Log | severity=debug | Log only, no notification | None | Test environment alerts, non-core components |
2.2 Tiered Routing Configuration
# Alertmanager routing configuration
route:
receiver: default
group_by: ['alertname', 'cluster', 'service']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
# P0 - Critical: immediate phone notification
- matchers:
- severity = critical
receiver: critical-phone
group_wait: 0s
repeat_interval: 30m # Repeat every 30 minutes
# P1 - Warning: IM channel notification
- matchers:
- severity = warning
receiver: warning-im
group_wait: 30s
repeat_interval: 2h
# P2 - Notice: email notification
- matchers:
- severity = info
receiver: info-email
group_wait: 5m
repeat_interval: 12h
# P3 - Log: no notification
- matchers:
- severity = debug
receiver: null
receivers:
- name: critical-phone
webhook_configs:
- url: 'http://phone-gateway/alert' # Phone notification gateway
send_resolved: true
- name: warning-im
webhook_configs:
- url: 'http://dingtalk-webhook/alert' # DingTalk/WeCom
- name: info-email
email_configs:
- to: 'ops-team@example.com'
- name: null
# No notification method configured
2.3 Alert Tiering Principles
Alert tiering decision tree:
Does the service provide core external functionality?
├── No → severity=info/debug (don't disturb on-call)
└── Yes → Does it affect many users (> 1% perceive)?
├── No → severity=warning
└── Yes → Does it make functionality completely unavailable?
├── No → severity=warning
└── Yes → severity=critical
Key principles:
- User impact as the standard: Not “CPU is high” → critical, but “users can’t place orders” → critical
- P0 alerts must be rare: No more than 2-3 P0s per week; otherwise, tiering is wrong
- Match notification channel to level: Phone for P0, IM for P1, email for P2
- Nighttime de-escalation: Reduce P1/P2 notification frequency outside business hours; P0 is not de-escalated
III. Problems with Threshold-Based Alerting
3.1 Traditional Threshold Alerting Pitfalls
# Typical threshold alerting rule
- alert: HighCPU
expr: 100 - (avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 5m
labels:
severity: warning
This rule looks fine, but in practice:
| Problem | Scenario | Consequence |
|---|---|---|
| Hard to set threshold | CPU 80% is normal for some services, dangerous for others | False positives or false negatives |
| Transient spikes | Scheduled tasks cause brief CPU spikes | Many invalid alerts |
| Capacity differences | Large instance at 50% load far exceeds small instance at 80% | Threshold doesn’t fit all instances |
| No user perspective | CPU 80% but users unaffected | Alert has no practical meaning |
| Missing context | Don’t know if this affects SLO | Can’t judge severity |
3.2 Multi-Level Threshold Approach
One improvement is configuring multi-level thresholds to reduce false positives:
groups:
- name: cpu-alerts
rules:
# P2 - Notice: CPU > 85%, sustained 30 minutes
- alert: HighCPUWarning
expr: |
100 - (avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
for: 30m
labels:
severity: info
annotations:
summary: "CPU usage elevated: {{ $labels.instance }}"
description: "CPU usage {{ $value }}% exceeds 85% for 30 minutes"
# P1 - Warning: CPU > 95%, sustained 5 minutes
- alert: HighCPUCritical
expr: |
100 - (avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 95
for: 5m
labels:
severity: warning
annotations:
summary: "CPU usage critical: {{ $labels.instance }}"
description: "CPU usage {{ $value }}% exceeds 95%, may impact service"
runbook: "https://wiki.internal/runbooks/high-cpu"
This is better than a single threshold, but still “guessing thresholds” — you don’t know if 85% or 95% actually corresponds to user-perceived problems.
IV. SLO-Based Alerting
4.1 What Is SLO-Based Alerting
SLO (Service Level Objective) is an alerting approach based on service quality objectives. The core philosophy: don’t alert on “a metric exceeded a threshold,” alert on “error budget is being consumed too fast.”
Error Budget = 1 - SLO target
Example: SLO = 99.9% availability
Error Budget = 1 - 0.999 = 0.1%
Allowable unavailability in a 30-day window = 30 × 24 × 60 × 0.1% = 43.2 minutes
SLO-based alerting doesn’t ask “is CPU high?” but “are we consuming error budget too quickly?”
4.2 Multi-Window Multi-Burn-Rate Alerting
Google SRE recommends the Multi-Window Multi-Burn-Rate strategy:
groups:
- name: slo-alerts
rules:
# Fast burn: 1h window consuming 2% error budget (14.4x burn rate)
# → Detect severe problems within 1 hour
- alert: SLOBurnRateFast
expr: |
(
sum(rate(http_requests_total{status=~"5.."}[1h])) /
sum(rate(http_requests_total[1h]))
) > (1 - 0.999) * 14.4
and
(
sum(rate(http_requests_total{status=~"5.."}[5m])) /
sum(rate(http_requests_total[5m]))
) > (1 - 0.999) * 14.4
for: 2m
labels:
severity: critical
slo: availability-999
annotations:
summary: "SLO error rate fast burn"
description: "Error rate in the past hour exceeds SLO by 14.4x; error budget expected to be exhausted within 2 hours"
runbook: "https://wiki.internal/runbooks/slo-burn"
# Slow burn: 6h window consuming 5% error budget (6x burn rate)
# → Detect sustained, non-sudden service quality degradation
- alert: SLOBurnRateSlow
expr: |
(
sum(rate(http_requests_total{status=~"5.."}[6h])) /
sum(rate(http_requests_total[6h]))
) > (1 - 0.999) * 6
and
(
sum(rate(http_requests_total{status=~"5.."}[30m])) /
sum(rate(http_requests_total[30m]))
) > (1 - 0.999) * 6
for: 15m
labels:
severity: warning
slo: availability-999
annotations:
summary: "SLO error rate slow burn"
description: "Error rate in the past 6 hours exceeds SLO by 6x; error budget expected to be exhausted within 5 days"
4.3 Multi-Window Multi-Burn-Rate Parameters
| Window Combination | Burn Rate | Budget Consumed | Alert Level | Purpose |
|---|---|---|---|---|
| 1h + 5m | 14.4x | 2% (1h) | critical | Fast detection of severe issues |
| 6h + 30m | 6x | 5% (6h) | warning | Detect sustained issues |
| 1d + 2h | 3x | 10% (1d) | warning | Long-term trend monitoring |
| 3d + 6h | 1x | 10% (3d) | info | Budget consumption tracking |
Multi-window multi-burn-rate logic:
Both short window + long window exceed threshold → alert
(Avoids false positives from transient short-window spikes while ensuring response speed)
4.4 SLO Alerting vs Threshold Alerting
| Dimension | Threshold Alerting | SLO Alerting |
|---|---|---|
| Focus | Whether a metric exceeds threshold | Whether user experience is affected |
| False positive rate | High (thresholds hard to set precisely) | Low (based on actual error rates) |
| Context | Missing | Present (error budget consumption progress) |
| User relevance | Indirect | Direct |
| Operational burden | High (frequent threshold tuning) | Low (SLO targets are stable) |
| Alert volume | High | Low (only alerts that affect SLO) |
Core philosophy: SLO alerting answers “do we need to act now to protect user experience?” rather than “does a number look bad?” This is a mindset shift from “infrastructure monitoring” to “user experience monitoring.”
V. Alert Inhibition and Aggregation
5.1 Inhibition Rules
When an upstream failure occurs, downstream services generate cascading alerts. Inhibition rules automatically silence lower-level related alerts when a higher-level alert fires.
# Alertmanager inhibit_rules
inhibit_rules:
# When a service is unreachable, inhibit CPU/memory alerts for that service
- source_matchers:
- alertname = ServiceDown
target_matchers:
- alertname =~ 'HighCPU|HighMemory|DiskSpaceWarning'
equal: ['service', 'instance']
# When a cluster-level alert fires, inhibit all instance alerts in that cluster
- source_matchers:
- alertname = ClusterDown
- severity = critical
target_matchers:
- severity =~ 'warning|info'
equal: ['cluster']
# When DB primary is down, inhibit replica sync lag alerts
- source_matchers:
- alertname = MySQLMasterDown
target_matchers:
- alertname = MySQLReplicationLag
equal: ['cluster']
5.2 Alert Aggregation (Grouping)
Use group_by to merge related alerts into a single notification, reducing notification volume:
route:
group_by: ['alertname', 'cluster', 'service'] # Group by alert name + cluster + service
group_wait: 30s # Wait 30s for first alert, collecting same-group alerts
group_interval: 5m # 5-minute interval for subsequent same-group alerts
repeat_interval: 4h # 4-hour repeat notification interval
Aggregation effect example:
Before aggregation (no group_by):
[10:00:01] High CPU - web-01
[10:00:02] High CPU - web-02
[10:00:03] High CPU - web-03
[10:00:05] High Memory - web-01
[10:00:06] High Disk - web-01
→ 5 separate notifications
After aggregation (group_by: ['alertname', 'cluster']):
[10:00:30] High CPU (web-01, web-02, web-03)
[10:05:00] High Memory (web-01) + High Disk (web-01)
→ 2 aggregated notifications
5.3 Alert Deduplication
In dual-replica Prometheus scenarios, both instances generate the same alerts. Alertmanager deduplicates via the Gossip protocol:
# Alertmanager startup parameters (cluster mode)
alertmanager \
--cluster.listen-address=0.0.0.0:9094 \
--cluster.peer=alertmanager-2:9094 \
--cluster.peer=alertmanager-3:9094 \
--cluster.gossip-interval=200ms \
--cluster.pushpull-interval=1m
VI. Alert Metrics and Governance
6.1 Key Alert Metrics
| Metric | Definition | Target | Calculation |
|---|---|---|---|
| Alert precision | Valid alerts / Total alerts | > 80% | Manual labeling + periodic audit |
| MTTR | Mean Time To Recovery | < 30min | Incident records |
| False positive rate | False alerts / Total alerts | < 10% | Alert + ticket matching |
| Alert volume | Daily total alerts | Trending down | Prometheus metrics |
| Alert silence rate | Silenced alerts / Total alerts | < 20% | Alertmanager API |
| P0 alert count | Weekly P0 alerts | < 5 | Alert records |
| Auto-resolution rate | Auto-resolved alerts / Total alerts | < 30% | Alertmanager metrics |
6.2 Measuring Alerts with Prometheus
# Alert metric rules
groups:
- name: alert-metrics
rules:
# Daily total alerts
- record: alerts:fired:total_per_day
expr: increase(ALERTS{alertstate="firing"}[24h])
# Alert count by severity
- record: alerts:fired:by_severity
expr: count by(severity) (ALERTS{alertstate="firing"})
# Auto-resolution rate
- record: alerts:auto_resolved:rate
expr: |
sum(rate(ALERTS_FOR_STATE{alertstate="resolved"}[1h])) /
sum(rate(ALERTS_FOR_STATE[1h]))
# P0 alert count (weekly)
- record: alerts:critical:per_week
expr: increase(ALERTS{alertstate="firing", severity="critical"}[7d])
6.3 Alert Governance Process
┌─────────────────────────────────────────────────────┐
│ Alert Governance Loop │
│ │
│ 1. Alert audit (monthly) │
│ │ │
│ ▼ │
│ 2. Classification: valid / false positive / │
│ noise / missing docs │
│ │ │
│ ▼ │
│ 3. Root cause analysis: bad threshold / missing │
│ inhibition / outdated rules │
│ │ │
│ ▼ │
│ 4. Optimization: adjust thresholds / add │
│ inhibition / delete rules / add docs │
│ │ │
│ ▼ │
│ 5. Effect validation: compare next month's │
│ alert volume and precision │
│ │ │
│ ▼ │
│ 6. Continuous iteration ←────────────────────────── │
└─────────────────────────────────────────────────────┘
6.4 Alert Audit Script
#!/bin/bash
# alert-audit.sh — Monthly alert audit report
AM_URL="http://alertmanager:9093"
PROM_URL="http://prometheus:9090"
echo "========== Monthly Alert Audit Report =========="
echo "Time range: $(date -d '1 month ago' '+%Y-%m-%d') ~ $(date '+%Y-%m-%d')"
echo ""
# Alert count by severity
echo "## Alert Count Statistics"
curl -s "$PROM_URL/api/v1/query" \
--data-urlencode 'query=sum by(severity)(increase(ALERTS{alertstate="firing"}[30d]))' | \
jq -r '.data.result[] | " \(.metric.severity): \(.value[1])"'
echo ""
# Top 10 high-frequency alerts
echo "## Top 10 High-Frequency Alerts"
curl -s "$PROM_URL/api/v1/query" \
--data-urlencode 'query=topk(10, sum by(alertname)(increase(ALERTS{alertstate="firing"}[30d])))' | \
jq -r '.data.result[] | " \(.metric.alertname): \(.value[1])"'
echo ""
# Auto-resolution rate
echo "## Auto-Resolution Rate"
curl -s "$PROM_URL/api/v1/query" \
--data-urlencode 'query=sum(rate(ALERTS{alertstate="resolved"}[30d])) / sum(rate(ALERTS[30d]))' | \
jq -r '.data.result[0].value[1] | " Auto-resolution rate: \(.* 100 | floor)%"' 2>/dev/null
echo ""
echo "=========================================="
VII. Alerting Rule Writing Standards
7.1 Alerting Rule Template
- alert: <AlertName> # CamelCase, concise and clear
expr: <PromQL> # Query expression
for: <duration> # Duration to avoid transient alerts
labels:
severity: <level> # critical/warning/info
service: <service> # Affected service
team: <team> # Responsible team
annotations:
summary: "<one-line description>" # Brief summary
description: "<detailed description>" # Include current value and impact
runbook: "<URL>" # Runbook link
dashboard: "<URL>" # Grafana dashboard link
7.2 Good Alert vs Bad Alert
# ✗ Bad alert: insufficient information, not actionable
- alert: HighCPU
expr: cpu_usage > 80
for: 5m
labels:
severity: warning
# ✓ Good alert: complete information, actionable
- alert: APIServerHighCPU
expr: |
100 - (avg by(instance)(rate(node_cpu_seconds_total{mode="idle", job="api-server"}[5m])) * 100) > 90
for: 10m
labels:
severity: warning
service: api-server
team: platform
annotations:
summary: "API Server CPU usage exceeds 90%"
description: "Instance {{ $labels.instance }} CPU usage {{ $value }}%, sustained 10 minutes, may impact API response latency"
runbook: "https://wiki.internal/runbooks/api-server-high-cpu"
dashboard: "https://grafana.internal/d/api-server?var-instance={{ $labels.instance }}"
7.3 Alerting Rule Review Checklist
| Check Item | Requirement |
|---|---|
| Alert name | Concise and clear, problem identifiable from name |
| Duration | Reasonable for time to avoid transient spikes |
| Severity label | Correct severity tier |
| Summary | One sentence explaining what happened |
| Description | Includes current value, impact scope, related instances |
| Runbook | Actionable handling steps document |
| Dashboard | Grafana dashboard link |
| Inhibition | Related inhibit_rules configured |
| Deduplication | Appropriate group_by added |
VIII. Practical Alert Noise Reduction Tips
8.1 Using for to Eliminate Transient Spikes
# Disk usage alert: for 15m to avoid log write spikes
- alert: DiskSpaceWarning
expr: |
(node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} /
node_filesystem_size_bytes{fstype!~"tmpfs|overlay"}) * 100 < 20
for: 15m # Alert only after 15 minutes sustained
labels:
severity: warning
8.2 Using Prediction Functions for Early Alerting
# Predict disk will fill within 24 hours
- alert: DiskWillFillIn24h
expr: |
predict_linear(node_filesystem_avail_bytes[2h], 24 * 3600) < 0
for: 1h
labels:
severity: warning
annotations:
summary: "Disk predicted to fill within 24 hours: {{ $labels.instance }}"
description: "Based on past 2-hour trend, {{ $labels.mountpoint }} will fill within 24 hours"
8.3 Using absent to Detect Missing Data
# Exporter unreachable causing missing data
- alert: ExporterDown
expr: absent(up{job="node-exporter"})
for: 5m
labels:
severity: critical
annotations:
summary: "All node-exporter data missing"
description: "Either the entire monitoring network is down, or all node-exporters are offline"
8.4 Time-Range-Aware Alerting
# Only alert non-critical issues during business hours
- alert: LowReplicaCountBusinessHours
expr: |
kube_deployment_status_replicas < kube_deployment_spec_replicas
and on() (hour() >= 8 and hour() < 22 and day_of_week() > 0 and day_of_week() < 6)
for: 10m
labels:
severity: warning
IX. From Alert to Action: Runbooks
9.1 The Value of Runbooks
The value of an alert lies not in the notification itself, but in how quickly it can be acted upon after notification. A Runbook (operations manual) bridges the gap between alert and action.
Alert notification → Runbook link → Diagnostic steps → Fix operations → Verify → Close alert
9.2 Runbook Template
# Runbook: API Server High CPU Usage
## Alert Information
- Alert name: APIServerHighCPU
- Severity: warning
- Impact scope: API response latency may increase
## Diagnostic Steps
### 1. Verify the alert
Check Grafana dashboard to confirm CPU is indeed sustained high
- Dashboard: https://grafana.internal/d/api-server
### 2. Check abnormal processes
```bash
ssh {{ $labels.instance }}
top -c -b -n 1 | head -20
3. Check request volume
Confirm whether there’s an abnormal traffic spike
curl -s http://localhost:9090/api/v1/query?query=rate(http_requests_total[5m])
Fix Operations
Case A: Traffic spike
- Check for abnormal request sources
- Consider temporarily scaling up Pod replicas
Case B: Resource leak
- Check Goroutine count
- Consider rolling restart of Pods
Escalation Path
- If unresolved within 30 minutes → escalate to P0 → notify architect team
## Summary
Building a high-quality alerting system is a continuous optimization process. Key takeaways:
- **Alerts are actions**: Every alert must have a clear action; alerts that can't be acted on should be deleted or demoted to logging
- **Tiering is the foundation**: A four-level system tells on-call engineers what to focus on and what to ignore
- **SLO is the direction**: Migrate from threshold-based to SLO-based alerting, centered on user experience
- **Inhibition reduces noise**: Leverage Alertmanager's inhibit and group_by to eliminate cascading alerts and duplicate notifications
- **Metrics drive governance**: Regularly audit alert precision, MTTR, and false positive rates; use data to drive continuous optimization
- **Runbooks close the loop**: Every alert should have a corresponding handling document; alert → diagnosis → fix → verification forms a closed loop
Alert governance is not a one-time project but a continuous iterative process. Monthly audits, periodic optimization, and continuous iteration are what transform an alerting system from a "noise machine" into a "reliable fault signal light."
## References & Acknowledgments
This article referenced the following materials during writing. We thank the original authors for their contributions:
1. [Prometheus Alerting Best Practices](https://prometheus.io/docs/practices/alerting/) — Prometheus Authors, referenced for Prometheus Alerting Best Practices