Skip to main content

🚨 Alerting Systems

Collecting metrics and logs is only half the job. If nobody is watching the dashboards at 3 a.m., you're still relying on angry users to tell you the site is down. Alerting closes the loop — it watches for you and pages a human when something genuinely needs attention. Done well, it catches problems early. Done badly, it trains your team to ignore it.

Week 13 · Day 4 (Thursday: Monitoring and Logging) · Lecture 3

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain why alerting on symptoms and SLOs beats alerting on causes
  • Write Prometheus alert rules with a threshold and a for duration window
  • Route, group, and inhibit alerts with Alertmanager to cut noise
  • Choose the right notification channel for each severity level
  • Design an on-call escalation path that never leaves an alert unanswered
  • Recognise and prevent alert fatigue so every page stays trustworthy

Estimated Time: 75 minutes

Practice: Write a symptom-based alert rule and an Alertmanager route that escalates critical alerts to a pager.

In This Lesson

Why Alert at All?

An alerting system is the smoke detector for your application. It sits quietly and watches your metrics and logs; when the pattern of "trouble" appears, it sounds the alarm — ideally before anyone smells smoke. The goal isn't to notify you of everything that happens. It's to notify you of the few things that need a human decision, right now.

graph TD A["Metrics"] --> C["Alerting Engine"] B["Logs"] --> C C --> D{"Condition met
for long enough?"} D -->|"No"| E["Stay quiet"] D -->|"Yes"| F["Fire Alert"] F --> G["Route & Group"] G --> H["Notify On-Call"] H --> I["Acknowledge & Fix"] H -->|"No response"| J["Escalate"]

Notice the diamond: a good alert doesn't fire the instant a number crosses a line. It waits to confirm the problem is real and sustained. That single idea — patience before paging — is what separates a trusted alerting system from a noisy one.

Alert on Symptoms & SLOs

Here's the most important principle in this lesson: alert on symptoms your users feel, not on internal causes.

A doctor doesn't wire a patient to alarms for every internal fluctuation — a slightly elevated white-cell count doesn't sound a siren. The alarms are for symptoms that matter: the heart stops, breathing fails. Your system is the same. High CPU is a cause; it might mean trouble, or it might just be a busy afternoon doing useful work. Users don't feel CPU. They feel slow responses and errors.

Alert on user-facing symptoms, treat internal causes as diagnostic context ✅ Alert on these error rate > SLO p99 latency too high requests failing (symptoms users feel) 📊 Diagnose with these CPU / memory queue depth disk I/O (causes, on dashboards)
Page on symptoms; keep the causes on dashboards to explain why once you're already looking.

SLOs make "how bad is too bad" concrete

An SLO (Service Level Objective) is a target you commit to, like "99.9% of requests succeed over 30 days." That budget of allowed failure — the error budget — turns vague worry into math. Alert when you're burning through the budget fast enough to miss the target, and you alert on exactly what matters, no sooner.

💡 The dashboard-vs-page distinction

If a signal requires a human to act now, it's an alert (a page). If it's useful context but nobody needs to wake up, it's a metric on a dashboard. Most things are dashboards. Be stingy with pages.

Writing Alert Rules

In the Prometheus world, alerts are just queries with a threshold and a duration. The for clause is the "patience" from our diagram — the condition must hold continuously before the alert fires, which filters out momentary blips.

# alert_rules.yml
groups:
  - name: service_slos
    rules:
      # SYMPTOM: users are getting errors. Fire if 5xx rate exceeds 5%
      # of traffic, sustained for 5 minutes (not a one-second spike).
      - alert: HighErrorRate
        expr: |
          sum(rate(http_requests_total{status=~"5.."}[5m]))
            / sum(rate(http_requests_total[5m])) > 0.05
        for: 5m
        labels:
          severity: critical
          team: backend
        annotations:
          summary: "Error rate above 5% on {{ $labels.service }}"
          description: "Current error rate: {{ $value | humanizePercentage }}"
          runbook: "https://wiki.example.com/runbooks/high-error-rate"

      # SYMPTOM: the site is slow. Fire if 95th-percentile latency
      # stays above 1s for 10 minutes.
      - alert: HighLatency
        expr: |
          histogram_quantile(0.95,
            sum(rate(http_request_duration_seconds_bucket[5m])) by (le)) > 1
        for: 10m
        labels:
          severity: warning
          team: backend
        annotations:
          summary: "p95 latency above 1s"
          description: "p95 is {{ $value }}s over the last 10 minutes"

✅ Every good alert rule has four things

  • A symptom-based expression (errors, latency) — not a raw cause
  • A for window so transient blips don't page anyone
  • A severity label to drive routing
  • An actionable annotation with a runbook link — the responder knows what to do

Routing, Grouping & Inhibition

Once an alert fires, Alertmanager decides who hears about it and how. Three features do the heavy lifting of noise reduction.

Routing — send it to the right team

Match on labels to direct alerts. Critical ones go to the pager; everything else goes to chat.

# alertmanager.yml
route:
  group_by: ['alertname', 'service']
  group_wait: 30s        # buffer briefly so related alerts arrive together
  group_interval: 5m     # wait before sending an update for a group
  repeat_interval: 4h    # re-notify a still-firing alert only every 4h
  receiver: 'team-chat'  # default
  routes:
    - matchers: [ severity="critical" ]
      receiver: 'pagerduty'
    - matchers: [ team="frontend" ]
      receiver: 'frontend-chat'

receivers:
  - name: 'pagerduty'
    pagerduty_configs:
      - routing_key: '<your-integration-key>'
  - name: 'team-chat'
    slack_configs:
      - api_url: '<your-slack-webhook>'
        channel: '#alerts'
        send_resolved: true

Grouping — one notification, not a hundred

If a database dies and forty services all error at once, you want one grouped notification ("40 services failing") rather than forty pages. group_by and group_wait handle this automatically.

Inhibition — silence the redundant

When a whole cluster is down, you don't also need "high latency" warnings for every service on it. Inhibition suppresses the lesser alert while the bigger one is firing:

inhibit_rules:
  # If a critical alert is firing, mute the matching warning-level one
  - source_matchers: [ severity="critical" ]
    target_matchers: [ severity="warning" ]
    equal: ['alertname', 'service']

Notification Channels & Severity

Match the intrusiveness of the channel to the urgency of the problem. Waking someone at 3 a.m. for a cosmetic issue is how you lose their trust; emailing a total outage is how you lose customers.

SeverityMeaningResponseChannel
P1 CriticalOutage or data-loss risk, all usersImmediately, 24/7Phone call / pager push
P2 HighMajor feature broken or badly degradedWithin ~30 min, 24/7Pager / SMS
P3 MediumMinor issue or early warningBusiness hoursChat (Slack/Teams)
P4 LowNon-urgent, trending toward a thresholdNext business dayTicket / email

⚠️ If it's not urgent, it's not a page

A page (phone/pager) interrupts a human's life. Reserve it for P1/P2 — things that genuinely can't wait. Push a P3 to chat and file a P4 as a ticket. Every page that turns out to be ignorable erodes the reflex to respond to the next one.

On-Call & Escalation

An alert that fires but is never acknowledged is worse than no alert — it creates a false sense of coverage. An escalation path guarantees that if the first responder doesn't answer, someone else does.

graph TD A["Critical Alert Fires"] --> B["Page Primary On-Call"] B -->|"Acknowledged"| C["Investigate & Fix"] B -->|"No response in 15 min"| D["Page Secondary On-Call"] D -->|"Acknowledged"| C D -->|"No response in 15 min"| E["Page Team Lead"] E -->|"Acknowledged"| C E -->|"Still no response"| F["Escalate to Management"]

Tools like PagerDuty and Grafana OnCall manage the schedule and the escalation timers. Alertmanager just hands the alert off with a routing key:

receivers:
  - name: 'pagerduty'
    pagerduty_configs:
      - routing_key: '<your-pagerduty-integration-key>'
        severity: '{{ .CommonLabels.severity }}'
        description: '{{ .CommonAnnotations.summary }}'

💡 Humane on-call

Rotate on-call fairly, keep shifts short, and make handoffs explicit. For global teams, "follow-the-sun" scheduling means nobody is paged at 4 a.m. A well-designed rotation is as much a people problem as a technical one — burned-out engineers respond slower to real emergencies.

Avoiding Alert Fatigue

Remember the boy who cried wolf. A system that pages constantly for non-issues trains its responders to ignore it — and then misses the one alert that mattered. Alert fatigue is a real, studied phenomenon: in hospitals, staff exposed to hundreds of alarms a day respond measurably slower to critical ones. The same applies to engineers.

Measure your alert quality

  • Signal-to-noise ratio — what fraction of alerts led to real action? Aim high.
  • Alert frequency — how many pages per on-call shift? More than a couple is a smell.
  • Time to acknowledge / resolve — rising times can mean people are tuning out.

Cut the noise

  • Use a for window so brief blips never page
  • Group related alerts into a single notification
  • Inhibit redundant alerts under a bigger one
  • Silence alerts during planned maintenance windows
  • Delete alerts that fire repeatedly without ever needing action

✅ Make every alert actionable

The test: when this fires, is there something a human should do? If yes, it's a good alert — attach a runbook. If the honest answer is "just watch it," it belongs on a dashboard, not in someone's pocket at midnight.

Practice & Quiz

🏋️ Exercise 1: A symptom-based alert rule

Goal: Write a Prometheus alert named ServiceDown that fires when a service's success rate — the fraction of non-5xx responses — drops below 95%, sustained for 3 minutes, at critical severity.

💡 Hint

Success rate is 1 - (5xx rate / total rate), or equivalently fire when the 5xx fraction exceeds 0.05. Add for: 3m and a severity: critical label.

✅ Solution
- alert: ServiceDown
  expr: |
    sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
      / sum(rate(http_requests_total[5m])) by (service) > 0.05
  for: 3m
  labels:
    severity: critical
  annotations:
    summary: "Success rate below 95% on {{ $labels.service }}"
    description: "5xx rate is {{ $value | humanizePercentage }}"

🏋️ Exercise 2: Route critical alerts to a pager

Goal: Add an Alertmanager route so that any alert with severity="critical" goes to a pagerduty receiver, while everything else falls through to the default chat receiver.

✅ Solution
route:
  receiver: 'team-chat'      # default for non-critical
  routes:
    - matchers: [ severity="critical" ]
      receiver: 'pagerduty'

receivers:
  - name: 'team-chat'
    slack_configs:
      - api_url: '<webhook>'
        channel: '#alerts'
  - name: 'pagerduty'
    pagerduty_configs:
      - routing_key: '<key>'

🎯 Quick Quiz

Question 1: Which is the better thing to alert on?

Question 2: What does the for: 5m clause on a Prometheus alert do?

Question 3: What is the main purpose of an escalation path?

Best Practices & Pitfalls

✅ Do

  • Alert on user-facing symptoms (errors, latency) and SLO burn, not raw causes
  • Add a for window so momentary spikes don't page anyone
  • Attach a runbook link and clear context to every alert
  • Match channel intrusiveness to severity — pager for P1, chat for P3
  • Define an escalation path so no critical alert goes unanswered
  • Review alert quality regularly and delete the noisy ones

❌ Don't

  • Page for every threshold cross, including harmless blips
  • Alert on causes like CPU when what users feel is latency and errors
  • Send non-urgent alerts through intrusive channels
  • Fire a hundred alerts for one root cause — group and inhibit instead
  • Leave alerts without an owner or a documented response

⚠️ The quietest alerting system is often the best

A team that gets two meaningful pages a week and acts on both has a far healthier system than one drowning in fifty daily alerts it has learned to swipe away. Fewer, better alerts win.

Summary

🎉 Key Takeaways

  • Alert on symptoms and SLOs users feel — not internal causes like CPU
  • Every rule needs a threshold, a for window, a severity, and an actionable runbook
  • Use Alertmanager routing, grouping, and inhibition to turn a storm into one clear notification
  • Match the channel to the severity — pager for critical, chat for minor
  • An escalation path guarantees coverage; ruthless pruning prevents alert fatigue

📚 Additional Resources

🚀 What's Next?

You've now completed Week 13's monitoring-and-logging arc — you can measure, understand, and be alerted about your system. Next you'll pull it all together in the Production Checklist: the final review of readiness, security, scaling, and reliability before you flip the switch and go live.

🎉 Alerts people actually trust.

Symptom-based, actionable, and quiet by default — that's an alerting system your team will answer at 3 a.m.