A health check without a threshold is a log line. These are the nine checks worth automating on a production Linux VPS, the exact cutoff for each, and the command that evaluates it. Every check returns exit code 0 when healthy and 1 when it is not, so a single cron job can run all nine and alert on any failure.
The Nine Checks and Their Cutoffs
| Check | Pass condition | Signal |
|---|---|---|
| Disk usage | < 85% on all real mounts | log growth, unrotated archives |
| Inode usage | < 85% | millions of tiny session/cache files |
| Available memory | > 10% of MemTotal | leaks, runaway workers |
| Swap in/out | 0 pages/s at steady load | host memory pressure |
| CPU steal | < 5% averaged over 5 min | noisy neighbour |
| Load per core | < 1.5 × nproc | queueing, slow queries |
| TCP retransmits | < 0.5% of segments out | MTU, NIC offload, congestion |
| Filesystem read-only | all mounts rw (except ro-design) | I/O errors, failing volume |
| Service responders | nginx, php-fpm, sshd, db reachable | crashed unit |
One Strict Shell Script, Nine Exit Codes
#!/usr/bin/env bash
# /usr/local/bin/vps-health.sh — exit 0 healthy, 1 unhealthy
set -uo pipefail
fail=0
note(){ printf '%s %s\n' "$(date -Is)" "$*"; }
bad(){ note "FAIL: $*"; fail=1; }
# 1. disk
while read -r pct mp; do
[ "${pct%\%}" -ge 85 ] && bad "disk ${mp} at ${pct}"
done < <(df -P --local -x tmpfs -x devtmpfs | awk 'NR>1{print $5, $6}')
# 2. inodes
while read -r pct mp; do
[ "${pct%\%}" -ge 85 ] && bad "inodes ${mp} at ${pct}"
done < <(df -Pi --local -x tmpfs -x devtmpfs | awk 'NR>1{print $5, $6}')
# 3. memory
avail=$(awk '/MemAvailable/{print $2}' /proc/meminfo)
total=$(awk '/MemTotal/{print $2}' /proc/meminfo)
awk -v a="$avail" -v t="$total" 'BEGIN{exit !(a/t < 0.10)}' && bad "MemAvailable $((avail/1024))MB of $((total/1024))MB"
# 4. swap activity (two samples 5s apart)
s1=$(awk '/pswpin/{print $2}' /proc/vmstat); sleep 5
s2=$(awk '/pswpin/{print $2}' /proc/vmstat)
[ $((s2 - s1)) -gt 0 ] && bad "swap-in $((s2-s1)) pages in 5s"
# 5. steal
u1=$(awk '/^cpu /{print $9}' /proc/stat); t1=$(awk '/^cpu /{print $2+$3+$4+$5+$6+$7+$8+$9+$10}' /proc/stat)
sleep 5
u2=$(awk '/^cpu /{print $9}' /proc/stat); t2=$(awk '/^cpu /{print $2+$3+$4+$5+$6+$7+$8+$9+$10}' /proc/stat)
awk -v d=$((u2-u1)) -v t=$((t2-t1)) 'BEGIN{exit !(100*d/t > 5)}' && bad "steal above 5%"
# 6. load per core
l1=$(cut -d' ' -f1 /proc/loadavg); n=$(nproc)
awk -v l="$l1" -v n="$n" 'BEGIN{exit !(l > 1.5*n)}' && bad "load $l1 on $n cores"
# 7. TCP retransmits in currently open sockets
ss -ti 2>/dev/null | grep -o 'retrans:[0-9/]*' | awk -F'[:/]' '$2>0{bad++} END{exit !(bad>0)}' && bad "active TCP retransmits detected"
# 8. read-only filesystems
mount | awk '$4 ~ /^ro/ && $3 !~ /(squashfs|iso9660)/{print}' | grep -q . && bad "filesystem mounted read-only"
# 9. service reachability
for u in http://127.0.0.1/ ; do
curl -sf -o /dev/null --max-time 5 "$u" || bad "endpoint $u not responding"
done
pgrep -x sshd >/dev/null || bad "sshd not running"
note "health check complete (exit $fail)"
exit $fail
Install it, run it by hand once to confirm the output, then schedule it. `systemd` timers give you journald integration for free and avoid cron’s environment surprises:
sudo install -m755 vps-health.sh /usr/local/bin/vps-health.sh
sudo tee /etc/systemd/system/vps-health.service >/dev/null <<'EOF'
[Unit]
Description=VPS health check
[Service]
Type=oneshot
ExecStart=/usr/local/bin/vps-health.sh
EOF
sudo tee /etc/systemd/system/vps-health.timer >/dev/null <<'EOF'
[Unit]
Description=Run VPS health check every 5 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
AccuracySec=30s
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload && sudo systemctl enable --now vps-health.timer
journalctl -u vps-health.service -n 40 --no-pager
Reading the Output Without Guesswork
The nine checks above will flag roughly four distinct root causes: runaway log or cache growth (checks 1–2), memory leaks (checks 3–4), hypervisor contention or genuine overload (checks 5–6), and network/kernel misconfiguration (checks 7–8). If only check 9 fires while 1–8 are clean, you are looking at an application or dependency failure, not a server one — go read the unit’s journal first. `journalctl -u
Establish a Baseline Before the First Alert
Thresholds above are generic. Your instance has its own normal, and an alert that fires during a nightly backup teaches you to ignore alerts. Capture a seven-day baseline first, then set your cutoffs relative to it:
# Collect five core metrics every minute for a week (no agent required)
sudo tee /etc/cron.d/vps-baseline >/dev/null <<'EOF'
* * * * * root /usr/local/bin/vps-snapshot.sh >/dev/null 2>&1
EOF
sudo tee /usr/local/bin/vps-snapshot.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
ts=$(date +%s)
steal=$(awk '/^cpu /{print $9}' /proc/stat)
load1=$(cut -d' ' -f1 /proc/loadavg)
avail=$(awk '/MemAvailable/{print $2}' /proc/meminfo)
swapin=$(awk '/pswpin/{print $2}' /proc/vmstat)
rootpct=$(df -P / | awk 'NR==2{gsub(/%/,"",$5); print $5}')
echo "$ts $steal $load1 $avail $swapin $rootpct" >> /var/log/vps-baseline.tsv
EOF
sudo chmod +x /usr/local/bin/vps-snapshot.sh
# After a week, look at the daily peaks rather than the mean:
awk 'BEGIN{max=0}{if($3>max)max=$3} END{print "peak load1:", max}' /var/log/vps-baseline.tsv
awk 'BEGIN{max=0}{if($6>max)max=$6} END{print "peak root%:", max}' /var/log/vps-baseline.tsv
Set the load cutoff at roughly 1.3x your observed peak, the memory floor at 60% of your observed minimum, and disk at 85% regardless of baseline — the filesystem-level cutoff is not negotiable. This turns a generic nine-check script into one that is quiet when your server is healthy and specific when it is not.
What Healthy Quiet Actually Looks Like
A well-tuned 2 vCPU / 4 GB VPS on a committed host, running nginx plus PHP-FPM plus MariaDB, idles at load 0.05–0.15, holds steal under 0.5%, keeps 40–60% of RAM in page cache, and never touches swap. If your idle signature is meaningfully worse than that, the problem predates any workload you have added — and the allocation mechanics in our published VPS benchmark dataset explain which host characteristics produce which idle signature.
Two thresholds people set wrong: load average and disk. Load average on a single-core VM of 1.5 is fine (waiting on I/O counts as runnable); 6.0 on eight cores is a real problem. And 85% disk, not 95%, because ext4 needs roughly 5% free to avoid fragmentation-driven slow writes, and logrotate needs room to do its job. If you are consistently near the disk threshold, the reclaim workflow in our VPS performance resources will buy you back the space, and the provider comparison is where to look if you have simply outgrown the volume size.
If you are tuning a small VPS and want hardware that does not fight you, our VPS provider performance tables break down CPU steal, NVMe IOPS, and RAM overcommit behaviour across the hosts we test on. Newer KVM nodes with dedicated vCPU pinning make the numbers in this article reproducible rather than aspirational. Two hosts we keep coming back to: InterServer VPS for flat-rate pricing with no RAM upcharge, and Cloudways managed cloud if you would rather not manage the kernel yourself. Compare the two against the benchmark methodology we publish before you commit.



Leave a Reply
You must be logged in to post a comment.