StackPractices
intermediate By Mathias Paulenko

Alert Management: On-Call Alerting That Works

A practical guide to alert management: cut alert fatigue, set severity levels, design escalation policies and sustainable on-call rotations.

Overview

Alerting is how your systems tell you something needs attention. Done poorly, it creates noise, burnout, and slower incident response. Done well, it gives the right person the right information at the right time so they can act decisively.

This guide covers alert design, severity classification, on-call structures, escalation policies, and the operational practices that keep an alerting program healthy over time.

When to Use

Alert management sits on top of the monitoring stack — if you’re still building that, start with the Complete Guide to Monitoring and Alerting and come back. Use this guide when:

  • Your team receives more than 5 alerts per person per week
  • Alerts are frequently ignored or treated as noise
  • Critical alerts are missed due to volume
  • You’re establishing or redesigning an on-call rotation
  • Alert fatigue is causing burnout or attrition

It doesn’t apply when you don’t have metrics or logs to alert on yet, or when incidents reach you through customer complaints — fix observability first. It’s also not the right tool for a two-person team shipping a side project: one paging rotation of one person is a burnout plan, not an on-call design.

Core Concepts

ConceptDescription
Alert FatigueDesensitization caused by too many low-value alerts
SeverityClassification of alert urgency (critical, warning, info)
EscalationAutomatically routing unacknowledged alerts to the next responder
On-Call RotationScheduled responsibility for incident response
Alert BudgetMaximum acceptable alert volume per time period
RunbookStep-by-step guide for responding to a specific alert

Severity Levels

Severity maps to user impact, not to how loud the metric is. Tie each level to what the responder must do and how fast — the incident response runbook template pairs well here.

LevelNameResponse TimeChannelExample
P1Critical5 minutesPage/SMSService down, revenue impact, data loss
P2High30 minutesPage/SlackDegraded performance, partial outage
P3Medium4 hoursSlack/EmailCapacity threshold, non-urgent anomaly
P4Low1-2 business daysTicketCleanup needed, non-urgent optimization
P5InfoNoneDashboard onlyMetrics for context, not action required

Severity Design Principles

  • P1 means drop everything and respond immediately
  • P2 means respond within the current working period
  • P3 and below don’t page; they create tickets or Slack messages
  • If everything is P1, nothing is P1
  • Review severity distribution monthly; aim for <10% P1

A useful test for any proposed alert: write down what the responder would actually do when it fires. If the honest answer is “look at a dashboard,” it belongs at P5 — on the dashboard, not the pager.

The Alert Lifecycle

Mermaid flowchart LR diagram

Step-by-Step Alert Management

1. Design Alerts That Matter

Every alert must be something you can act on and user-impacting:

# Example: Prometheus alert rules with severity

groups:
  - name: service_alerts
    rules:
      # P1: User-facing service is down
      - alert: ServiceDown
        expr: up{job=~"api|web|payment"} == 0
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.job }} is down"
          runbook_url: "https://wiki/runbooks/service-down"

      # P2: Elevated error rate but service still responding
      - alert: HighErrorRate
        expr: |
          (
            sum(rate(http_requests_total{status=~"5.."}[5m]))
            /
            sum(rate(http_requests_total[5m]))
          ) > 0.05
        for: 5m
        labels:
          severity: high
        annotations:
          summary: "High error rate in {{ $labels.service }}"

      # P3: Capacity warning — no immediate action needed
      - alert: DiskSpaceWarning
        expr: (node_filesystem_avail_bytes / node_filesystem_size_bytes) < 0.15
        for: 10m
        labels:
          severity: medium
        annotations:
          summary: "Disk space low on {{ $labels.instance }}"

      # P4: Informational — track but don't page
      - alert: HighMemoryUsage
        expr: (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) < 0.1
        for: 30m
        labels:
          severity: low
        annotations:
          summary: "High memory usage on {{ $labels.instance }}"

Alert Design Checklist

  • Alert on symptoms users feel (errors, latency), not causes (disk full)
  • Every P1/P2 alert must have a runbook link
  • Use for: duration to prevent flapping (require sustained failure)
  • Include summary and description that tell the responder what to check
  • Add labels for service, environment, team, and severity

Two patterns keep volume down without losing coverage. Alert on symptoms, not causes: ServiceDown fires when users see errors; DiskSpaceWarning stays at P3 because a disk at 85% hurts nobody today. Deduplicate by incident: if 50 pods die at once, the responder needs one page (“deployment X unavailable”), not fifty.

2. Build On-Call Rotations

Design rotations that are fair and sustainable:

Rotation TypeBest ForStructure
Primary/SecondarySmall teams (3-6)One primary, one backup
Follow-the-sunGlobal teams8-hour shifts across time zones
Weekly rotationMedium teams (6-12)One week on, 3-5 weeks off
Daily rotationLarge teams (12+)One day on, rest of week off

Rotation Guidelines

  • Limit on-call frequency to no more than 1 week in 4
  • Ensure handoff between shifts includes active incidents
  • Compensate for on-call time (pay or time off)
  • Allow opt-out for personal events with coverage
  • Track and review incident frequency per rotation

The handoff deserves its own checklist: a shared doc or Slack thread listing open incidents, active silences, and anything that fired but wasn’t resolved before the shift ended. Handoffs that live in the outgoing responder’s head are how incidents get lost between shifts.

Ramp new responders in stages rather than throwing them the pager: a week shadowing (they watch, the primary acts), a week as secondary (they get the escalation page), then primary with a named backup who knows they’re on their first rotation. Nobody learns the on-call job from a wiki page — they learn it watching someone triage a real page at 2am.

3. Define Escalation Policies

Ensure unacknowledged alerts reach a human:

Escalation Path Example:

Alert Fires
    → Primary on-call (page + SMS)
        → Acknowledged? (stop)
        → Not acknowledged in 5 min
            → Secondary on-call (page)
                → Acknowledged? (stop)
                → Not acknowledged in 10 min
                    → Engineering Manager (page)
                        → Not acknowledged in 15 min
                            → Director of Engineering (page)

Escalation Principles

  • Escalate quickly for P1 (5-10 minute intervals)
  • Escalate more slowly for P2 (30-60 minute intervals)
  • Include the previous responder in the escalation chain
  • Set up team-wide Slack channels for visibility
  • Log all escalations for post-incident review

Set the timeout by severity and by reality: if your primary is asleep or underground, five minutes is already generous for a P1. Anything past three hops almost never reaches a human who can help — fix the rotation instead of lengthening the chain.

4. Create Runbooks for Every Alert

A runbook turns an alert into a solvable problem:

# Runbook: ServiceDown

### Alert
ServiceDown: `{{ $labels.job }}` is down

### Impact
Users can't access `{{ $labels.job }}`. Revenue impact if payment or API.

### Diagnosis Steps
1. Check service health endpoint: `curl http://{{ $labels.instance }}/health`
2. Check if pod is running: `kubectl get pods -l app={{ $labels.job }}`
3. Check recent deployments: `kubectl rollout history deployment/{{ $labels.job }}`
4. Check resource usage: `kubectl top pod -l app={{ $labels.job }}`
5. Check logs: `kubectl logs -l app={{ $labels.job }} --tail=100`

### Resolution Steps
1. If pod crashed: `kubectl rollout restart deployment/{{ $labels.job }}`
2. If resource exhausted: Scale deployment or node pool
3. If deployment caused issue: `kubectl rollout undo deployment/{{ $labels.job }}`
4. If dependency down: Check dependency status and escalate to owning team

### Escalation
If unresolved in 15 minutes, escalate to: platform-team@company.com

Runbook Guidelines

  • One runbook per P1/P2 alert
  • Include diagnosis, resolution, and escalation steps
  • Link runbook directly in alert notification
  • Review and update runbooks quarterly
  • Measure how well runbooks work (time to resolve when followed)

The alert runbook template gives you this structure ready to copy. The part that gets skipped is the last bullet — actually measuring whether the runbook shortened resolution, so bad runbooks get fixed instead of trusted.

5. Reduce Alert Fatigue

Actively measure and reduce alert volume:

MetricTargetAction if Exceeded
Alerts per person per week< 5Tune thresholds, remove noisy alerts
P1 alerts per month< 2Fix root causes, not symptoms
Alert acknowledgment time< 5 min for P1Improve runbooks, training
False positive rate< 10%Increase for: duration, add conditions
Alerts without runbooks0Create missing runbooks

Fatigue Reduction Tactics

  • Consolidate: group related alerts into one notification
  • Suppress: silence known maintenance windows — always with an expiry date
  • Deduplicate: one alert per incident, not per affected host
  • Auto-remediate: auto-restart, auto-scale for known recoverable issues
  • Delete: remove alerts that fire more than once without action

Use Alertmanager’s inhibit_rules (or your tool’s equivalent) to stop cascade noise: when a P1 like ServiceDown fires, inhibit the dependent warnings that only repeat the same fact. Silences work the same way — they’re for planned maintenance, not for hiding alerts you haven’t gotten around to fixing.

Designing Thresholds

The threshold is where alerting lives or dies. A number that’s too tight pages people for normal variance; too loose and the incident is over before anyone looks.

Start from the baseline, not a guess: pull the last 4 weeks of the metric and look at the p95/p99 of normal operation. Set the alert just past the worst of “normal” — if error rate normally peaks at 2%, alert at 5%, not at 3%. For metrics with strong daily or weekly cycles, a single static number will always be wrong for part of the day; use a percentile-based alert (quantile_over_time) or a deviation alert that compares the current value to the same time yesterday.

Build in hysteresis so alerts can’t flap at the boundary: fire when error rate exceeds 5% for 5 minutes, and only mark recovered when it drops below 3%. Most tools implement this with separate fire/recover thresholds or with for: plus a second resolved expression. The gap between the two numbers is the difference between a useful alert and a metronome.

Decide who owns the threshold while you’re at it. The team that carries the pager should set the number — they pay the cost of being wrong in both directions. When a platform team owns the rules but a product team carries the pages, thresholds drift toward whatever made the dashboard pretty, and the responder inherits the gap. Put the alert file in the same repo as the service, review it in the same PR process, and let the on-call team veto a rule that can’t name its action.

One more guard worth having: a deploy-time review question. Any PR that adds an alert answers “what does the responder do when this fires?” in the description. Rules that can’t answer it get sent back — it’s the cheapest review question in the whole process and it filters out most of the alerts that would otherwise live and die as noise.

Silences, Inhibitions and Notification Design

Silences and inhibitions are the two tools that keep correlated noise from multiplying a single incident into fifty pages. They’re different mechanisms: a silence mutes an alert for a window (planned maintenance, known-bad deploy); an inhibition mutes lower-severity alerts while a higher-severity one is already firing.

# Alertmanager: inhibit warnings when the critical alert already covers them
inhibit_rules:
  - source_match:
      alertname: ServiceDown
      severity: critical
    target_match:
      severity: high
    equal: ["service"]  # only silence warnings about the same service

And the notification itself deserves design. A page that says “HighErrorRate” is noise. A page that says “api checkout: 8% errors for 5 min — runbook: — dashboard: — silence: ” is actionable. Every notification should carry four things: what’s broken, how severe, where to start diagnosing (runbook link), and a one-click way to acknowledge or silence. If your paging tool can’t render links, at least include the runbook URL in the annotation so it survives into Slack and email.

A last habit: annotate alerts with deploy context. A label or annotation carrying the latest deploy revision (deployment_revision) turns “errors went up” into “errors went up 6 minutes after release 2026-10-24-14” — that’s the difference between a page and a diagnosis.

Alert Budgets

Treat pages like an error budget for humans. Pick a weekly budget per responder — five actionable alerts is a sane default — and treat overspend the way you’d treat burning error budget: stop the feature work and pay down the cause. If last week burned 9 pages against a budget of 5, the retro question isn’t “why were you slow” but “which alerts spent the budget and how do we get it back.”

The budget works because it converts a fuzzy feeling (“we get too many pages”) into a number a manager can act on. Publish it, review it in the weekly meeting, and make the on-call responder’s report of it take two minutes, not twenty. Teams that track the budget discover the same thing every time: three or four rules produce most of the noise, and fixing them recovers the whole budget.

Decide up front what overspend costs. The usual policy: while the budget is blown, alert-debt work jumps the queue ahead of feature work — the same rule SRE teams apply to error budgets. Without that consequence the number is just a chart people nod at. And keep the accounting honest: a page counts against the budget even when it “resolved itself,” because it still cost someone an interruption.

A Worked Example: Fixing a Noisy Alert

A concrete pass at the CPUHigh rule from the audit table above — it fired 41 times in 90 days, nobody acknowledged it, nobody acted on it:

# Before: static threshold, no context, fires every batch run
- alert: CPUHigh
  expr: avg(rate(node_cpu_seconds_total{mode!="idle"}[5m])) > 0.8

# After: deviation from baseline, sustained, with context
- alert: CPUHigh
  expr: |
    avg(rate(node_cpu_seconds_total{mode!="idle"}[5m]))
      > 1.5 * avg(rate(node_cpu_seconds_total{mode!="idle"}[1h] offset 1d))
  for: 15m
  labels:
    severity: medium
  annotations:
    summary: "CPU on {{ $labels.instance }} is 50% above yesterday's baseline"
    runbook_url: "https://wiki/runbooks/high-cpu"

The changes: baseline-relative expression so nightly batch spikes stop paging, for: 15m so a 30-second burst dies quietly, a severity it actually deserves (medium — nobody needs waking for high CPU), and a summary that tells the responder what changed rather than restating the metric name. If it still fires with nobody acting after a month of the new version, it gets deleted — that’s the rule.

Companion files: the alert-management-guide companion repo has the full alert rules YAML, the inhibit_rules config, and the ServiceDown runbook as standalone files.

Auditing Your Alert Inventory

Every few months, export your alert rules and score each one against three questions: did it fire in the last quarter, did anyone act on it, and did that action change anything? The alerts that fail all three are the ones making the rest invisible.

Alert audit output (example):

ALERT                    | Fires/90d | Acked | Acted | Verdict
ServiceDown              | 2         | 2     | 2     | keep
HighErrorRate            | 7         | 6     | 5     | keep
DiskSpaceWarning         | 18        | 3     | 1     | retune threshold
CPUHigh                  | 41        | 0     | 0     | delete
SlowQueries              | 12        | 4     | 0     | fold into latency alert

Track two health metrics for the program itself: MTTA (mean time to acknowledge — should stay under your escalation timeout) and MTTR (mean time to resolve — should trend down as runbooks improve). If MTTA is fine but MTTR keeps climbing, your problem is runbooks and diagnosis, not alerting.

Make the audit part of incident follow-up too. Every P1 or P2 postmortem should answer three alerting questions: did the right alert fire, at the right severity, and did its runbook actually help? When the answer to any of them is no, the fix lands in the alerting backlog with an owner — not in a “someday” list that never gets read.

What works

  • Alert on symptoms, not causes — a full disk is a cause, slow requests are the symptom users feel.
  • Every alert must demand action; if the honest response is “wait and see,” it doesn’t get to page anyone.
  • Use for: so a metric has to stay breached before it wakes someone — flapping alerts are noise with a schedule.
  • Separate paging from logging: interesting isn’t the same as urgent.
  • Review the inventory monthly — which alerts fire, which get acknowledged, which get ignored — and kill or fix the ignored ones.
  • Pay people for carrying the pager: on-call is work, and treating it as free is how you lose responders.
  • Keep the whole loop boring: fire → acknowledge → runbook → fix → adjust the alert. The moment any step needs improvisation, that’s where the next incident gets expensive.

Common Mistakes

  • Alerting on everything: more alerts don’t mean better coverage, they mean more noise until nobody listens.
  • No escalation path: the primary doesn’t respond and the alert dies in silence while production burns.
  • Missing runbooks: an alert without one forces the responder to guess at 3am, and the guess is usually wrong.
  • Ignoring fatigue: high volume burns out responders until the critical alert that matters gets missed with the rest.
  • Static thresholds on cyclical metrics: CPU spikes during batch jobs are normal — alert on deviation from baseline or you’ll page someone every night at 2am for nothing.

Variants

  • No-ops alerting: fully automated remediation for known recoverable failures (OOM restarts, disk cleanup). Works well for a small set of well-understood faults; dangerous anywhere a wrong auto-action makes things worse.
  • Severity-based routing: different channels per severity (Slack for P3+, PagerDuty for P1/P2). Cheap to implement and usually the first customization worth doing.
  • Team-specific ownership: alerts route to whichever team owns the service. Requires a maintained ownership map — without one, alerts bounce to the wrong inbox and die there.
  • AI-assisted alerting: anomaly detection that adapts thresholds to the baseline instead of static numbers. Useful on cyclical metrics; keep a static floor so a degrading baseline doesn’t silently raise the bar.

Troubleshooting

  • Alert fires and resolves constantly (flapping): increase for: duration, widen the threshold band, or alert on a rate/derivative instead of the raw value.
  • Alert never fires even though it should: check the expression against real data in the metrics UI, verify the for: duration isn’t longer than the incident window, and confirm the alertmanager routing actually reaches a receiver.
  • Escalation never engages: the primary acknowledges but doesn’t act — shorten the ack-to-resolve expectation and review who got paged. Escalations that never fire are usually an ownership problem, not a config problem.
  • Silence expires and the alert comes back: the root cause was never fixed. Convert recurring silences into tickets with owners, or accept deleting the alert.
  • Same incident pages 30 hosts: configure group_by on the common label (service, cluster, alertname) and add a group_wait so the notification arrives as one.
  • Runbook exists but responders don’t use it: link it inside the alert annotation (runbook_url), keep it under 10 steps, and prune steps that no longer match reality — stale runbooks train people to ignore them.

Further Reading

Frequently Asked Questions

How many alerts per week is too many?

More than five that demand action per person per week. If you're paging more than once a week, the problem is the thresholds or the system — not the responder.

Should I alert on CPU and memory usage?

Usually no — they're symptoms, not user impact. Alert on latency, error rate, and saturation unless the resource directly gates user requests.

How do I handle a noisy alert I can't fix right now?

Silence it with an expiry date — never indefinitely — and file a ticket for the root cause with a sprint attached. A silenced alert without a fix date is a deleted alert.

What's the difference between an alert and a dashboard?

Alerts interrupt; dashboards explain. If it doesn't require a human to act now, it belongs on a dashboard, not a pager.

How often should escalation policies trigger?

15-30 minutes for high-severity, or the second responder never engages. Longer timeouts train the primary to snooze; shorter ones burn out the backup.

What causes alert fatigue?

Volume without value — alerts that fire often and mean little. Every ignored alert teaches the team to ignore the next one, including the one that matters.