Alert rules that are not noise
An alert earns its place only if you can finish the sentence "when this fires I will…". Everything else is a dashboard panel. Twenty-five rules you read beats eighty you have trained yourself to dismiss, and the way you get there is a written reason next to every threshold.
The lab this material came from had eleven alerts firing continuously. Not one of them described something anybody was going to do. Everything was, technically, being monitored. Nothing was being noticed. That is the failure worth designing against, and it is not caused by bad rules — it is caused by accumulating rules, each of which looked reasonable on the day it was added.
The one rule: no alert without an action
Before a rule goes in the file, write the sentence that starts "when this fires I will…". If you cannot finish it, you have found a dashboard panel. Put it on a dashboard. A panel you look at when you are curious costs nothing. A notification you have taught yourself to swipe away costs you every real alert that arrives after it.
The corollary is unpopular and correct: an alert nobody acts on gets deleted or fixed, never silenced indefinitely. A permanent silence is a deleted alert that still burns evaluation time and still pads the count so you feel covered.
Write the reason next to the number
Every threshold is a trade between hearing about a problem early and being woken by something that
would have fixed itself. That trade is a judgement, and judgements evaporate. Six months later you
are looking at for: 15m with no idea whether 15 was reasoning or a default, so you leave
it alone — or worse, you widen it during an incident and never narrow it back.
So the reason lives in the rules file as a comment beside the number. Here is what that looks like for the rules I keep, and the reasoning is the point of the table, not the numbers:
| Alert | Threshold | for: | Why that number |
|---|---|---|---|
| Instance down | up == 0 | 5m | Swallows a reboot, a DHCP renewal, a container restart. Under 2m this is the single biggest source of false pages in a home lab. |
| Systemd unit failed | any failed unit | 15m | Units that fail once at boot and get restarted by a dependency are common. 15m filters those without hiding one that stays down. |
| Disk space warning | 85% | 30m | 85% still leaves room to think, and the window rides out a big temporary file — backup staging, a large download — that comes back down on its own. |
| Disk space critical | 95% | 10m | Past 95%, failures start looking like unrelated application bugs. The noise filtering already happened at the warning tier. |
| Disk will fill in 72 h | 7-day trend, <30% free | 2h | A 6-hour regression window reads ordinary hourly write noise as a crisis and a genuine two-week linear drain as flat. 7 days smooths it and still gives three days of warning. |
| Filesystem out of inodes | 90% | 30m | "No space left on device" on a 40%-full disk is the most confusing failure in a lab. One cheap rule deletes the whole category. |
| Memory pressure | 90% of MemAvailable | 15m | MemAvailable, never MemFree: a healthy Linux box has almost no free memory because the kernel caches with it. |
| Host OOM kill | any kill in 10m | 0m | A past event, not a state. There is nothing to confirm by waiting. |
| Job stale | one missed slot | 10m | Worth looking at in the morning, not worth waking for. |
| Job dark | badly overdue or never ran | 10m | The only signal a stopped job ever produces. |
| Backup too old | 36 h since success | 30m | A daily backup gets one full missed slot plus twelve hours of slack. Tighter pages you for a slow run; looser loses two days silently. |
| Proxmox quorum lost | quorate == 0 | 2m | Never benign, never self-heals quietly. Short window on purpose. |
| Guest stopped | up == 0 | 15m | Warning, not critical: plenty of guests are off on purpose. Put those in the expression's exclusion list, with a dated comment. |
| Endpoint down | probe_success == 0 | 10m | Home internet, DNS and reverse proxies blip. 10m is the line between a useful alert and one that fires on every lease renewal. |
| Certificate expiring | 21 days | 1h | Long enough that two automated renewals have already failed, so it means "renewal is broken", not "renewal has not run yet". |
| Notifications failing | any failure in 15m | 10m | Alerts firing with nobody being told is the worst possible state, and it is invisible from inside the alert list. |
Two patterns run through that column. Short windows are for conditions that never self-heal — quorum loss, an OOM kill that already happened. Long windows are for conditions with a normal, boring version that resolves itself: a reboot, a unit restarted by a dependency, a temporary file. Everything else sits between those two poles, and where it sits is the thing you write down.
Four moves, in order
1. Delete
Go through the currently firing list and finish "when this fires I will…" for each one. Delete every rule that fails the sentence. This is usually about half of them, and it is the only step in the whole process with no downside.
2. Deduplicate at the source
One event should produce one notification. The commonest source of duplicates in a homelab is not
the alert router — it is a cluster exporter. Every Proxmox node's exporter reports on every guest and
every storage in the cluster, so a naive sum() over a three-node cluster gives you three
times the truth, and your alert says nine guests are unprotected when three are. Wrap cluster-wide
series in max by (id) (...).
Two rules for one condition is the other duplicate: a warning at 85% and a critical at 95% both fire at 96%. That is what inhibition is for, not a reason to drop one tier.
3. Group and delay
route:
group_by: ['alertname', 'severity'] # one message per alert type, not per host
group_wait: 30s # collect the stragglers of a single event
group_interval: 5m # a new host joining an existing group waits this long
repeat_interval: 12h # do not re-tell me the same thing every four hours
repeat_interval is the setting people leave at four hours and then wonder why they
stopped reading notifications. Twelve or twenty-four hours is right for a lab. A problem you have not
fixed in twelve hours will not be fixed because you were reminded a fourth time.
Note the direction of the grouping choice: let the router group per-host detail into one message rather than aggregating it away inside the rule. That way one NFS server falling over is one notification that still lists the twelve affected mounts, instead of twelve messages or one message that hides which mounts they were.
4. Inhibit the consequences
Inhibition suppresses alerts caused by another firing alert. It is the difference between "the NFS server is down" and forty pages about mounts.
inhibit_rules:
# A critical about a thing silences the warning about the same thing.
- source_matchers: [severity = "critical"]
target_matchers: [severity = "warning"]
equal: ['alertname', 'instance']
# A host that is down is not also interestingly out of memory.
- source_matchers: [alertname = "InstanceDown"]
target_matchers: [severity =~ "warning|info"]
equal: ['instance']
# A storage that is down explains every stale backup job on it.
- source_matchers: [alertname = "ProxmoxStorageDown"]
target_matchers: [alertname =~ "BackupTooOld|JobStale|JobDark"]
Three tiers with three real behaviours
| Severity | Delivery | Meaning |
|---|---|---|
critical | Push notification, any hour | Something is broken or about to break and you are the fix. |
warning | Batched, quiet hours suppressed | Handle it tomorrow. |
info | Recorded only, never notified | Useful in a review, never worth an interruption. |
If everything you have is critical, nothing is. A good sign the tiers are working: you have
info rules that have never sent you a message and you would still not delete them.
Watch the watcher
Every de-noised system has one hole left. A monitoring stack that is down is perfectly quiet, and quiet is what health looks like. Internal rules cover the internal cases — rule evaluation failing, notifications failing — but nothing inside the stack can report the stack being off.
For that you need a check from outside the failure domain, on a second small host or a free external heartbeat service, that alerts when the monitoring server stops answering and deliberately does not route through your alert manager, because the alert manager is one of the things being watched. Fifteen lines of shell, a counter file so three consecutive failures are required and a restart is not an incident, and it covers the one failure your alerting system can never report on itself.
Fifteen minutes a quarter
Put a recurring review on the calendar. Per rule: how many times did it fire, how many of those were worth acting on, and does it still have an action? A rule at zero out of ten gets its threshold changed or gets deleted — and if you change the threshold, the new reason goes next to the new number, same as the first one did.
Fifteen minutes a quarter is the whole maintenance cost of keeping the list at twenty-five rules you read instead of eighty you have stopped seeing.
The full rule file — 25 rules across seven groups, each with its reasoning as a comment and a pointer into the incident runbook — plus a working router config, the dead-man's-switch script and the quarterly alert-review worksheet are in the Homelab Monitoring Kit ($49 minimum / $59 suggested). Free updates forever. 30-day 100% refund, no questions.