15 Prometheus alert rules for Linux servers (copy-paste, with thresholds explained)
A production-ready node_exporter rules file: disk-fill prediction, OOM kills, CPU steal, inode exhaustion, read-only filesystems, failed systemd units, clock skew and more - each with the threshold reasoning, the for: duration, and the false-positive traps.
The AlertKick team
Every team ends up writing the same fifteen node_exporter alert rules; here they are, so you can skip to the incidents. Each rule below includes the threshold reasoning and the for: duration, because a rule without a tuned for: is a pager-noise generator. Drop them into /etc/prometheus/rules/node.yml, run promtool check rules, reload Prometheus, and Alertmanager does the rest. (Need the stack first? Prometheus install -> node_exporter setup.)
The rules file skeleton
# /etc/prometheus/rules/node.yml
groups:
- name: node
rules:
# rules from every section below go here
The one rule that guards all the others
- alert: InstanceDown
expr: up{job="node"} == 0
for: 2m
labels: {severity: critical}
annotations:
summary: "{{ $labels.instance }} is not being scraped - host, exporter, or network is down"
Without this, a dead host produces fewer alerts, not more - every other rule goes silent exactly when things are worst. for: 2m rides out a single failed scrape.
Disk: trajectory, capacity, inodes, read-only
- alert: DiskFillPredicted
expr: predict_linear(node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"}[1h], 4*3600) < 0
and node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} < 0.25 * node_filesystem_size_bytes
for: 15m
labels: {severity: critical}
annotations:
summary: "{{ $labels.instance }} {{ $labels.mountpoint }} full within 4h at current rate"
- alert: DiskAlmostFull
expr: node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes < 0.10
for: 30m
labels: {severity: warning}
annotations:
summary: "{{ $labels.instance }} {{ $labels.mountpoint }} under 10% free"
- alert: InodesExhausted
expr: node_filesystem_files_free{fstype!~"tmpfs|overlay"} / node_filesystem_files < 0.10
for: 30m
labels: {severity: warning}
annotations:
summary: "{{ $labels.instance }} {{ $labels.mountpoint }} under 10% inodes free - deletes fail with disk space still showing"
- alert: FilesystemReadOnly
expr: node_filesystem_readonly{fstype!~"tmpfs|overlay"} == 1
for: 1m
labels: {severity: critical}
annotations:
summary: "{{ $labels.instance }} {{ $labels.mountpoint }} remounted read-only - usually a dying disk"
DiskFillPredicted is the star: trajectory, not percentage (a runaway log writer kills a disk from 40% in an hour; a stable 85% partition is fine forever). The extra and clause stops it firing on huge, mostly-empty volumes where a linear fit extrapolates nonsense. FilesystemReadOnly gets for: 1m because the kernel only does that remount when something is genuinely wrong underneath - it is never noise.
Memory: pressure and the OOM killer
- alert: OutOfMemory
expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.10
for: 10m
labels: {severity: warning}
annotations:
summary: "{{ $labels.instance }} under 10% memory available"
- alert: OomKillDetected
expr: increase(node_vmstat_oom_kill[10m]) > 0
labels: {severity: critical}
annotations:
summary: "{{ $labels.instance }} kernel OOM-killed a process - something died and it wasn't chosen by you"
- alert: SwapThrashing
expr: rate(node_vmstat_pswpin[5m]) + rate(node_vmstat_pswpout[5m]) > 1000
for: 10m
labels: {severity: warning}
annotations:
summary: "{{ $labels.instance }} is swapping heavily - performance is already degraded"
Use MemAvailable, never MemFree - Linux deliberately keeps free memory low and MemFree alerts on healthy page-cache behaviour. OomKillDetected has no for: on purpose: it is a fact about the past, not a state that might pass.
CPU: saturation, steal, load
- alert: HighCpu
expr: 1 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) > 0.90
for: 15m
labels: {severity: warning}
annotations:
summary: "{{ $labels.instance }} CPU above 90% for 15m"
- alert: CpuStealHigh
expr: avg by (instance) (rate(node_cpu_seconds_total{mode="steal"}[5m])) > 0.10
for: 15m
labels: {severity: warning}
annotations:
summary: "{{ $labels.instance }} losing {{ $value | humanizePercentage }} to CPU steal - noisy neighbour or oversold host"
- alert: HighLoad
expr: node_load15 / count by (instance) (node_cpu_seconds_total{mode="idle"}) > 2
for: 15m
labels: {severity: warning}
annotations:
summary: "{{ $labels.instance }} load is 2x core count - work is queueing"
All three use for: 15m - short CPU spikes are life, sustained saturation is a problem. Steal deserves its own rule because it looks like generic slowness but has exactly one fix (move the VM), and knowing that during an incident saves an hour of profiling the wrong thing.
The quiet failures: units, clocks, network
- alert: SystemdUnitFailed
expr: node_systemd_unit_state{state="failed"} == 1
for: 5m
labels: {severity: warning}
annotations:
summary: "{{ $labels.instance }} unit {{ $labels.name }} is in failed state"
- alert: ClockSkew
expr: abs(node_timex_offset_seconds) > 0.05
for: 10m
labels: {severity: warning}
annotations:
summary: "{{ $labels.instance }} clock off by {{ $value }}s - TLS, auth tokens, and log ordering all suffer"
- alert: NetworkInterfaceErrors
expr: rate(node_network_receive_errs_total[5m]) + rate(node_network_transmit_errs_total[5m]) > 0.01
for: 15m
labels: {severity: warning}
annotations:
summary: "{{ $labels.instance }} {{ $labels.device }} showing interface errors - cable, NIC, or driver"
- alert: DiskIoSaturated
expr: rate(node_disk_io_time_seconds_total[5m]) > 0.90
for: 15m
labels: {severity: warning}
annotations:
summary: "{{ $labels.instance }} {{ $labels.device }} at IO saturation - everything on this disk is waiting"
SystemdUnitFailed requires --collector.systemd on node_exporter (covered in the setup guide) and is the cheapest coverage you will ever add: one rule alerts on any service that dies, including the ones you forgot you were running. That is 15 - validate and load:
promtool check rules /etc/prometheus/rules/node.yml && sudo systemctl reload prometheus
What no internal rule can catch
Every rule above shares one blind spot: it runs inside the network, on data the host reports about itself. Four incident classes are invisible from there - the site unreachable from the internet while every internal check is green, the SSL certificate expiring, the domain quietly lapsing, and the death of the Prometheus/Alertmanager stack itself, which by definition cannot page about its own absence. The fix costs nothing: AlertKick’s free external monitors check HTTP, SSL, DNS, and domain expiry from outside with real escalation (Slack, Telegram, SMS, phone, on-call rosters), and a one-line heartbeat from the Prometheus host alerts if the watcher itself stops watching. Internal rules for depth, external checks for truth - run both.
Frequently asked questions
- Where do Prometheus alert rules go?
- In YAML files referenced by rule_files: in prometheus.yml - a common layout is /etc/prometheus/rules/*.yml. Validate with promtool check rules <file>, then reload Prometheus. The rules fire in the Prometheus UI; Alertmanager handles routing them to Slack, email, or on-call.
- What is the for: duration in a Prometheus alert rule?
- How long the expression must stay true before the alert fires. It is the main false-positive control: a 30-second CPU spike is a scrape artifact, 15 minutes of saturation is an incident. Short for: on things that are instantly wrong (host down, filesystem read-only), long for: on things that are only wrong when sustained.
- Why alert on predict_linear instead of a disk usage percentage?
- Because 85% full means nothing without a fill rate: a log partition can live at 85% for a year, while a runaway process takes a disk from 40% to dead in an hour without ever tripping a static threshold in time to act. predict_linear alerts on trajectory - full within N hours - which is the thing you actually care about.
- Do these alert rules catch everything that can take a server down?
- No - they cover what the host can observe about itself. A dead Prometheus, an expired SSL certificate, a lapsed domain, or the site being unreachable from the internet are all invisible from inside. Pair internal rules with external uptime checks and a heartbeat on the monitoring stack itself.