The alert that fired for two years and meant nothing

Thirty-one alert rules and a notification channel that every member of the team had muted. That is a monitoring system that has been switched off by consensus, and no amount of adding rules to it improves anything.

The symptom

one month, by rule:

  rule                        fired   acted on
  node_cpu > 80% for 5m         188       0
  disk_used > 70%                94       0
  http_5xx_rate > 0.1% for 1m    41       1
  replica_lag > 5s               22       0
  memory_used > 85%              18       0
  queue_depth > 1000             14       0
  ... 22 more rules              31       1
  checkout_error_rate > 2%        4       4

  total                         412       6

One rule out of thirty-one has a hundred per cent action rate and it is the last one — the only one phrased in terms of something a customer would notice. Everything above it describes a machine, and a machine being busy is not by itself a problem.

Why it happens

Alerts are added after incidents, by the person who has just been surprised, and they are never removed after the underlying cause is fixed. Each one is individually reasonable and the set is unusable.

The fix

Counting first

# every firing is already recorded. the query exists.
sum by (alertname) (
  count_over_time(ALERTS{alertstate="firing"}[30d])
)

# and the second number, which is not recorded anywhere:
# whether anybody did something. we added a reaction
# convention in the channel — ✅ for acted on, 🔇 for
# ignored — and counted by hand for a month.

The action rate is the number that decides everything and no tool records it, so it has to be gathered deliberately. A month of emoji reactions is unscientific and produced a clear enough answer that nobody argued with the conclusion.

Deleting twenty-three rules

for each rule, one question: what would we have missed?

  node_cpu > 80%       nothing. every incident that
                       mattered also breached a latency
                       objective, which we now alert on.
  disk_used > 70%      nothing urgent — this is a
                       capacity report, not an alert.
                       → moved to the daily summary,
                         with a separate 90% page.
  replica_lag > 5s     nothing. it is a symptom of a
                       long transaction, which shows up
                       as latency.
  memory_used > 85%    nothing. the JVM-shaped
                       assumption behind this rule does
                       not apply to php-fpm.

deleted 23. moved 3 to reports. rewrote 5.

Writing down what would have been missed is what makes deletion defensible rather than reckless, and it is the artefact that survives the person who did it. Two of the twenty-three were genuinely hard to argue about and were kept for another quarter, then deleted.

Symptom over cause

# before: five rules describing possible causes
- alert: HighCpu
- alert: HighMemory
- alert: SlowQueries
- alert: QueueDepth
- alert: ReplicaLag

# after: one rule describing the promise
- alert: CheckoutLatencyObjectiveBurning
  expr: |
    histogram_quantile(0.95,
      sum by (le) (rate(http_duration_seconds_bucket{
        route="checkout"}[5m]))) > 1.5
  for: 5m
  annotations:
    summary: "checkout p95 {{ $value | humanizeDuration }}"
    runbook: "https://wiki.internal/runbooks/checkout-slow"

The five cause-rules become dashboard panels, which is where they belong — they are what somebody looks at after the symptom alert fires. The symptom rule cannot tell you what is wrong and it can tell you that something is, which is the only thing an alert needs to do.

The runbook requirement

the policy: an alert may not be created without a
runbook link, and the runbook must contain at least one
imperative sentence.

what happened when it was applied to the survivors:

  6 of 8   a runbook was written in under an hour
  2 of 8   could not get past "investigate the cause"
           → both were deleted. "investigate" is a
             dashboard, not a procedure.

and the effect on new alerts: 4 proposed in the
following quarter, 2 created. the other two died at
the runbook stage, correctly.

Alerting on absence

# the category nobody adds: a thing that stopped
- alert: NightlyBackupDidNotRun
  expr: |
    time() - max(backup_last_success_timestamp_seconds) > 93600
  for: 10m
  annotations:
    summary: "no successful backup in 26 hours"

- alert: MetricsStopped
  expr: absent(up{job="app"}) or
        (time() - max(app_heartbeat_timestamp) > 300)

Every alert we had was for a value being too high, and none for a value not arriving. The second rule is the one that guards the first: a monitoring system reporting nothing looks identical to a healthy system, and that is the failure mode with no natural symptom.

Routing by severity, honestly

two destinations, and the rule for choosing:

  page      a human is woken. reserved for: the service
            cannot serve, or money is being lost.
            → 3 rules.

  channel   somebody looks at it during working hours.
            → 5 rules.

and nothing else exists. an "informational" severity is
a dashboard panel with an alert's clothes on, and every
one we had was in the muted channel.

Verifying it worked

# three months after
  rules:              8   (was 31)
  firings/month:     14   (was 412)
  acted on:          12   (was 6)
  action rate:       86%  (was 1.5%)
  channel muted by:   0   (was everybody)

# and the test that mattered: a real incident in November
  22:14  CheckoutLatencyObjectiveBurning fires, pages
  22:15  acknowledged
  22:19  cause found via the runbook's first step
  22:31  resolved

# under the old system the same incident would have
# produced 9 alerts in a muted channel.

The action rate going from one and a half per cent to eighty-six is the measurement, and the November incident is the proof — a single page that woke the right person, with a runbook whose first step found the cause in four minutes.

What this costs

A gap somebody will point at after the next outage. Twenty-three deleted rules is twenty-three chances to say “we used to alert on that”, and the answer has to be the written record of what each would have caught — which is why the record exists.

The set will also grow back. Every incident produces pressure for a new rule, and the runbook requirement is the only thing standing against it — a requirement that is one exception away from being a formality, and the exception will be requested during an incident by somebody senior.