Skip to content
ExponaBLOG
PROMETHEUS, GRAFANA & MONITORING · DAY 016 / 100

Alert fatigue: how to fix it — best practices explained

Fewer, better alerts.

Expona Admin7 min read

Alert fatigue: how to fix it — best practices explained

You wake up to 47 Slack messages from your monitoring system. Most of them resolve themselves within minutes. By the time you've scrolled through half of them, you're already dismissing notifications without reading. This is alert fatigue, and it's one of the most damaging problems in modern observability. When your team ignores alerts because there are too many, the one that actually matters will slip through unnoticed.

Alert fatigue doesn't just waste time—it erodes trust in your monitoring system. Engineers stop checking dashboards, on-call rotations become demoralizing, and real incidents get buried in noise. The solution isn't to turn off alerts or lower your standards. It's to be deliberate about what you measure and when you interrupt people. We'll walk through the practical strategies that actually work.

Alert on symptoms users feel

The most common mistake is alerting on metrics that don't matter to anyone but the infrastructure team. High CPU usage on a database server sounds important until you realize it can spike to 90% and still serve requests in 10 milliseconds. Meanwhile, a 100-millisecond response time degradation—something your users definitely notice—might not trigger any alert at all.

Start by thinking like a customer. What outcomes do they depend on? Page load time. Transaction success rate. API availability. The ability to complete a checkout. Those are the symptoms that matter. If your database CPU is high but users aren't affected, it's not an emergency. If users are seeing errors, it is.

Consider a payment processing system. Instead of alerting on database replication lag in milliseconds, alert when the percentage of transactions completing within SLA drops below 99.5%. Instead of alerting on queue depth, alert when payment callbacks are delayed by more than 5 minutes and users can't confirm their purchase status.

This shift requires you to instrument your applications for business outcomes, not just infrastructure metrics. Add custom instrumentation to track the metrics your users care about:

yaml
groups:
  - name: payment_experience
    rules:
      - alert: PaymentLatencySLOBreach
        expr: |
          (
            sum(rate(payment_requests_duration_seconds_bucket{le="2"}[5m])) 
            / sum(rate(payment_requests_duration_seconds_count[5m]))
          ) < 0.995
        for: 2m
        annotations:
          summary: "Less than 99.5% of payments completing within 2s SLA"
          
      - alert: PaymentErrorRateHigh
        expr: |
          sum(rate(payment_requests_total{status="error"}[5m])) 
          / sum(rate(payment_requests_total[5m])) > 0.01
        for: 1m
        annotations:
          summary: "Payment error rate exceeds 1% threshold"

Notice the alerts are tied to user-visible outcomes. The payment latency alert fires when the SLA is breached, not when latency crosses an arbitrary threshold. The error rate alert is specific enough to matter but not so low that every blip triggers a page.

Delete alerts nobody acts on

Most alert rules in production systems are there by accident. They were created for a specific incident six months ago, they fired once or twice, and then they became noise. Nobody deleted them, so they've been sitting there ever since, training your team to ignore the alert system.

Start a audit. For each alert rule, ask: If this fired right now, what would we actually do? If you don't have a clear answer, delete it. If the answer is "check what's happening and then decide," that alert is too vague—either make it more specific or remove it.

Track alert statistics over 30 days. Count how many times each alert fired. Look at the ratio of alerts to actions taken. If an alert fired 100 times and the team took action once, something is wrong. Either the threshold is too sensitive, or the alert shouldn't exist.

Create a simple tracking sheet or query your Prometheus alert manager logs:

yaml
# Query Alertmanager to see how many times each alert was triggered
# in the last 30 days and how many were resolved by manual intervention
SELECT 
  labels->>'alertname' as alert_name,
  COUNT(*) as total_firings,
  SUM(CASE WHEN status = 'firing' THEN 1 ELSE 0 END) as unresolved,
  SUM(CASE WHEN acknowledged THEN 1 ELSE 0 END) as acknowledged_count
FROM alerts_history
WHERE timestamp > now() - interval '30 days'
GROUP BY labels->>'alertname'
ORDER BY total_firings DESC;

The alerts at the top of that list—the ones firing constantly with few acknowledgments—those are your noise. Either reconfigure them or delete them. Be aggressive. You're not losing anything valuable; you're removing obstacles to noticing real problems.

Tune thresholds with history

Alert thresholds are rarely set correctly on the first try. But most teams set them once and never adjust them. The threshold that made sense when you had 100 users breaks when you scale to 10,000. The CPU threshold that was perfect for your old hardware is useless after a migration.

Use your historical data to inform thresholds. Pull the 95th and 99th percentiles of your metrics over the last three months of normal operation. Set alert thresholds above the 99th percentile. If your API latency normally runs at p99 of 150ms, don't alert until it hits 500ms or 1,000ms—that's a real departure from normal.

This approach assumes you have baseline data. You do. You've been collecting metrics for weeks or months. Use it.

A concrete example: say you're alerting on database connection pool utilization. Instead of a hard threshold like "alert when pool is 80% full," query your actual usage patterns:

yaml
# Find the 99th percentile of normal connection pool utilization
# to inform your alert threshold
histogram_quantile(0.99, 
  max_over_time(
    db_connection_pool_utilization[30d]
  )
)

If that returns 65%, then alerting at 75% is reasonable—it's a significant deviation from your normal pattern. If it returns 92%, then 75% is too low and will create false positives.

Review and adjust thresholds quarterly. Seasonal patterns matter. Traffic that's normal in November is a disaster in December. Your thresholds should reflect your actual business patterns, not arbitrary engineering guesses.

Page only for urgent problems

Not every alert needs to interrupt someone at 3 AM. Page on incidents that require immediate human attention to prevent or mitigate user impact. Everything else should be logged, aggregated, and reviewed during business hours.

Define clear severity levels. In your alert manager, route high-severity alerts to PagerDuty. Route medium and low severity alerts to Slack channels, emails, or dashboards that the team reviews during work hours.

High severity (page immediately) should be reserved for:

  • Service is fully down or unavailable to most users
  • Data loss is actively occurring
  • Security incident is in progress
  • Revenue-impacting functionality is broken

Medium severity (Slack notification, review in morning standup):

  • Degradation affecting a portion of traffic
  • Non-critical infrastructure failing
  • Performance below SLA but not catastrophic

Low severity (recorded in logs, visible on dashboards):

  • Approaching quota limits
  • Deprecated API endpoints still in use
  • Non-critical health checks failing intermittently

Your alert manager configuration might look like this:

yaml
route:
  receiver: 'default'
  group_by: ['alertname', 'service']
  group_wait: 10s
  group_interval: 5m
  repeat_interval: 12h
  routes:
    - match:
        severity: critical
      receiver: 'pagerduty'
      repeat_interval: 5m
      continue: true
      
    - match:
        severity: warning
      receiver: 'slack-ops'
      repeat_interval: 2h
      continue: true
      
    - match:
        severity: info
      receiver: 'slack-logs'
      repeat_interval: 24h

Be ruthless about what qualifies as "critical." If you page someone more than once per week on average, your threshold is too low. On-call rotations only work when people trust that pages mean something genuinely urgent.

Common mistakes

Setting thresholds on derived metrics without context. Alerting on "CPU above 80%" doesn't mean anything without knowing your workload profile. Alert on the business impact instead: "requests per second below baseline" or "error rate above SLA."

Alerting on infrastructure instead of behavior. Your team doesn't care that a pod is pending. They care that users can't log in. Instrument for behavior, not infrastructure.

Creating alerts without a playbook. Every alert should have a clear playbook or runbook attached. If you don't know what to do when it fires, delete it.

Never retiring old alerts. Alerts should be treated like code—they need maintenance, reviews, and cleanup. Schedule quarterly audits to remove noise.

Alerting on everything to be safe. More alerts does not equal better safety. It equals worse safety, because your team stops paying attention. Fewer, better alerts work.

Key takeaways

  • Alert on symptoms users experience, not infrastructure metrics. If the system is degraded but users aren't affected, it's not urgent.
  • Audit your alert rules regularly. If nobody has acted on an alert in 30 days, delete it. Alert fatigue is worse than missing data.
  • Set thresholds based on historical percentiles, not guesses. Use your actual data to determine what "abnormal" looks like for your system.
  • Reserve pages for genuine emergencies. Route everything else to asynchronous channels. On-call rotation survival depends on trust that a page means something critical.
  • Treat alerts like code. Review them, refactor them, retire them. Your monitoring system should be as clean as your application code.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles