Building System Health Check and Alert Scripts
Collect load, memory, and disk metrics and trigger threshold-based alerts from scheduled scripts.
Building System Health Check and Alert Scripts is a free DevOps Bootcamp lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the DevOps Bootcamp learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why System Health Checks Matter
Production servers can degrade silently. CPU spikes, memory leaks, and full disks cause outages — but only if nobody notices in time. System health check scripts automate the monitoring loop: collect metrics, compare against thresholds, and fire alerts before users feel the pain.
- Scheduled via
cron, they run every few minutes without human attention - They produce consistent, timestamped output suitable for log aggregation
- Threshold-based logic keeps alerts meaningful — not every hiccup pages the on-call team
In this lesson you will build a production-grade health check script from scratch, layer by layer, covering load average, memory pressure, and disk utilisation.
Capturing Load Average
Linux exposes the 1-minute, 5-minute, and 15-minute load averages through /proc/loadavg and the uptime command. For scripting, /proc/loadavg is the cleanest source — no locale issues, no parsing variation across distros.
The snippet below reads the 1-minute load average and stores it in a variable for threshold comparison. cut extracts the first field; awk strips the decimal for integer comparison using bc for float arithmetic.
#!/usr/bin/env bash
# Read 1-minute load average from /proc/loadavg
LOAD_RAW=$(cut -d' ' -f1 /proc/loadavg)
echo "Raw load average: $LOAD_RAW"
# Number of CPU cores — used to normalise load
CPU_CORES=$(nproc)
echo "CPU cores: $CPU_CORES"
# Compute load percentage (load / cores * 100) using bc
LOAD_PCT=$(echo "scale=2; $LOAD_RAW / $CPU_CORES * 100" | bc)
echo "Load %: $LOAD_PCT"Threshold Comparison with Floating-Point Values
Bash cannot compare floating-point numbers natively — [ 1.5 -gt 1.2 ] throws an error. The two idiomatic solutions are:
bc— outputs1(true) or0(false) from a comparison expressionawk— can evaluate float conditions inside a pipeline
Using bc keeps the logic readable and easily testable. The pattern $(echo "$A > $B" | bc) returns 1 when the condition holds, which you test with [ ... -eq 1 ].
#!/usr/bin/env bash
LOAD_RAW=$(cut -d' ' -f1 /proc/loadavg)
CPU_CORES=$(nproc)
THRESHOLD=80 # alert when load % exceeds 80%
LOAD_PCT=$(echo "scale=2; $LOAD_RAW / $CPU_CORES * 100" | bc)
# bc returns 1 if the expression is true
if [ "$(echo "$LOAD_PCT > $THRESHOLD" | bc)" -eq 1 ]; then
echo "ALERT: Load is ${LOAD_PCT}% (threshold ${THRESHOLD}%)"
else
echo "OK: Load is ${LOAD_PCT}%"
fiCollecting Memory Metrics
/proc/meminfo is the authoritative source for memory statistics on Linux. Key fields:
MemTotal— total physical RAM in kBMemAvailable— estimated kB available for new allocations without swapping (better thanMemFree)
awk with a pattern match is the cleanest way to extract these values. Dividing MemAvailable by MemTotal and subtracting from 100 gives the used memory percentage, which drives your alert threshold.
#!/usr/bin/env bash
# Extract memory figures from /proc/meminfo (values in kB)
MEM_TOTAL=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)
MEM_AVAIL=$(awk '/^MemAvailable:/ {print $2}' /proc/meminfo)
# Used memory percentage
MEM_USED_PCT=$(echo "scale=2; (1 - $MEM_AVAIL / $MEM_TOTAL) * 100" | bc)
echo "Total RAM : ${MEM_TOTAL} kB"
echo "Available : ${MEM_AVAIL} kB"
echo "Used : ${MEM_USED_PCT}%"Collecting Disk Usage Metrics
The df command reports filesystem usage. For scripting, two flags are essential:
-h— human-readable sizes (for display only; avoid in arithmetic)--output=pcent,target— machine-parseable columns (GNU coreutils)
Iterating over all mounted filesystems lets the script flag any partition that is critically full, not just /. The % sign is stripped with tr -d '%' before integer comparison.
#!/usr/bin/env bash
DISK_THRESHOLD=85
# Skip header line with tail -n +2
# --output=pcent,target gives "85% /var" style lines
df --output=pcent,target | tail -n +2 | while read -r USED_PCT MOUNT; do
# Remove the % sign for arithmetic
USED_INT=${USED_PCT//%/}
if [ "$USED_INT" -ge "$DISK_THRESHOLD" ]; then
echo "ALERT: Disk $MOUNT is ${USED_PCT} full"
else
echo "OK : Disk $MOUNT is ${USED_PCT} full"
fi
doneStructured Alert Output with Timestamps
Alert messages without timestamps are nearly useless in log files or email reports. A consistent prefix makes log parsing trivial with grep or log-shipping agents.
Define a small alert function at the top of your script. It prepends an ISO-8601 timestamp, a severity level, and the check name. All alerts write to both stdout and a log file via tee.
date -u +"%Y-%m-%dT%H:%M:%SZ"— UTC timestamp, locale-independent- Writing to
stderrfor ALERT and tostdoutfor OK separates signal from noise in pipelines
#!/usr/bin/env bash
LOG_FILE="/var/log/healthcheck.log"
alert() {
local LEVEL="$1" # OK | WARN | ALERT
local CHECK="$2"
local MSG="$3"
local TS
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
local LINE="[$TS] [$LEVEL] [$CHECK] $MSG"
if [ "$LEVEL" = "ALERT" ]; then
echo "$LINE" | tee -a "$LOG_FILE" >&2
else
echo "$LINE" | tee -a "$LOG_FILE"
fi
}
# Usage examples
alert "OK" "DISK" "/ is 42% full"
alert "ALERT" "DISK" "/var is 91% full"Sending Email Alerts with mail and sendmail
The simplest on-server alerting mechanism is email via the local MTA (postfix, sendmail, or msmtp). The mail command (from mailutils or bsd-mailx) composes and dispatches a message in one line.
-s— subject line- Pipe the body through
stdin - On servers without a local MTA, replace
mailwith acurlcall to a transactional email API
Guard the send with a deduplication lock so one noisy condition does not flood the inbox.
#!/usr/bin/env bash
ALERT_EMAIL="ops@example.com"
LOCK_DIR="/tmp/healthcheck_locks"
mkdir -p "$LOCK_DIR"
send_alert() {
local CHECK="$1"
local MSG="$2"
local LOCK="$LOCK_DIR/${CHECK}.lock"
# Only send if no lock exists (prevents repeated emails within the hour)
if [ ! -f "$LOCK" ]; then
echo "$MSG" | mail -s "[ALERT] $CHECK on $(hostname)" "$ALERT_EMAIL"
touch "$LOCK"
# Lock expires after 1 hour via cron or find+delete
echo "Alert sent for $CHECK"
else
echo "Alert suppressed for $CHECK (lock active)"
fi
}
send_alert "HIGH_LOAD" "Load average exceeded 80% on $(hostname) at $(date)"Composing the Full Health Check Script
Now combine all three checks — load, memory, disk — into a single cohesive script with configurable thresholds at the top. This is the pattern used in production sysadmin automation:
- Constants declared at the top for easy tuning without editing logic
- Each check isolated in a function for readability and unit-testability
- A
mainfunction orchestrates the calls - Exit code
1if any alert fired,0otherwise — makes the script composable with monitoring frameworks like Nagios/Icinga
#!/usr/bin/env bash
set -euo pipefail
# ── Thresholds ───────────────────────────────────────────
LOAD_THRESHOLD=80 # percent of CPU capacity
MEM_THRESHOLD=90 # percent used
DISK_THRESHOLD=85 # percent used
ALERT_EMAIL="ops@example.com"
LOG_FILE="/var/log/healthcheck.log"
ALERT_FIRED=0
# ── Helpers ──────────────────────────────────────────────
ts() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
log() { echo "[$(ts)] $*" | tee -a "$LOG_FILE"; }
alert() { log "ALERT: $*"; echo "$*" | mail -s "[ALERT] $(hostname)" "$ALERT_EMAIL" 2>/dev/null; ALERT_FIRED=1; }
# ── Checks ───────────────────────────────────────────────
check_load() {
local raw cores pct
raw=$(cut -d' ' -f1 /proc/loadavg)
cores=$(nproc)
pct=$(echo "scale=2; $raw / $cores * 100" | bc)
if [ "$(echo "$pct > $LOAD_THRESHOLD" | bc)" -eq 1 ]; then
alert "Load ${pct}% exceeds ${LOAD_THRESHOLD}%"
else
log "OK load=${pct}%"
fi
}
check_memory() {
local total avail pct
total=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)
avail=$(awk '/^MemAvailable:/{print $2}' /proc/meminfo)
pct=$(echo "scale=2; (1 - $avail / $total) * 100" | bc)
if [ "$(echo "$pct > $MEM_THRESHOLD" | bc)" -eq 1 ]; then
alert "Memory ${pct}% used (threshold ${MEM_THRESHOLD}%)"
else
log "OK memory=${pct}%"
fi
}
check_disk() {
df --output=pcent,target | tail -n +2 | while read -r used mnt; do
local pct_int=${used//%/}
if [ "$pct_int" -ge "$DISK_THRESHOLD" ]; then
alert "Disk $mnt at ${used}"
else
log "OK disk $mnt=${used}"
fi
done
}
main() {
log "=== Health check START ==="
check_load
check_memory
check_disk
log "=== Health check END (alerts=$ALERT_FIRED) ==="
exit "$ALERT_FIRED"
}
mainScheduling with Cron
A health check script only provides value when it runs automatically. cron is the standard Unix scheduler. Edit the system-wide crontab or a dedicated file in /etc/cron.d/ to schedule your script.
- Run every 5 minutes:
*/5 * * * * - Always use absolute paths in cron —
$PATHis minimal in the cron environment - Redirect output to prevent cron from emailing every run:
>> /var/log/healthcheck.log 2>&1 - Use
MAILTO=""at the top of the crontab to silence cron's own email
# /etc/cron.d/healthcheck
# Run the health check every 5 minutes as root
MAILTO=""
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
*/5 * * * * root /usr/local/sbin/healthcheck.sh >> /var/log/healthcheck.log 2>&1Preventing Alert Storms with Cooldown Locks
When a threshold is breached continuously, a naive script fires an alert every 5 minutes — dozens of emails before an engineer can respond. A cooldown lock suppresses repeated alerts for a configurable window.
The pattern: write a lock file on first alert; skip subsequent alerts while the file is newer than the cooldown period; find with -mmin checks file age atomically without date arithmetic.
#!/usr/bin/env bash
LOCK_DIR="/tmp/hc_locks"
COOLDOWN_MIN=60 # suppress repeat alerts for 60 minutes
mkdir -p "$LOCK_DIR"
should_alert() {
local check="$1"
local lock="$LOCK_DIR/${check}.lock"
if [ ! -f "$lock" ]; then
# No lock — allow alert and create lock
touch "$lock"
return 0 # true: send alert
fi
# Lock exists — check if it is older than the cooldown
# find returns the filename only if it's OLDER than COOLDOWN_MIN
local expired
expired=$(find "$lock" -mmin +"$COOLDOWN_MIN" 2>/dev/null)
if [ -n "$expired" ]; then
touch "$lock" # refresh lock timestamp
return 0 # cooldown expired — allow alert
fi
return 1 # still within cooldown — suppress
}
# Usage
if should_alert "HIGH_LOAD"; then
echo "Sending load alert..."
# mail -s "..." ops@example.com <<< "Load too high"
else
echo "Load alert suppressed (cooldown active)"
fiTesting and Validating Your Health Check Script
Before deploying, validate the script in three ways:
- Syntax check:
bash -n healthcheck.shcatches parse errors without executing - Trace mode:
bash -x healthcheck.shprints every command as it executes — invaluable for debugging - Threshold override: temporarily lower thresholds to near-zero so the script triggers alerts on a healthy host, confirming the alert path works end-to-end
For the email path, redirect mail to a log file during testing using a MOCK_MAIL flag:
#!/usr/bin/env bash
# Smoke-test the alert path without sending real email
MOCK_MAIL=true
ALERT_EMAIL="ops@example.com"
send_mail() {
local subject="$1"
local body="$2"
if [ "$MOCK_MAIL" = true ]; then
echo "[MOCK MAIL] To: $ALERT_EMAIL | Subject: $subject"
echo "[MOCK MAIL] Body: $body"
else
echo "$body" | mail -s "$subject" "$ALERT_EMAIL"
fi
}
# Override threshold to guarantee an alert fires
LOAD_THRESHOLD=0 # Any load will exceed 0%
LOAD_RAW=$(cut -d' ' -f1 /proc/loadavg)
CPU_CORES=$(nproc)
PCT=$(echo "scale=2; $LOAD_RAW / $CPU_CORES * 100" | bc)
if [ "$(echo "$PCT > $LOAD_THRESHOLD" | bc)" -eq 1 ]; then
send_mail "[ALERT] Load on $(hostname)" "Load is ${PCT}%"
fiKnowledge Check: Cooldown Strategy
Consider the following scenario: your health check cron job runs every 5 minutes. Disk usage on /var crosses 85% and stays there for 3 hours. You want the on-call engineer to receive an alert once per hour, not every 5 minutes. Which implementation strategy is most appropriate?
Lesson Recap: System Health Check Scripts
In this lesson you built a complete, production-ready system health check and alerting pipeline. Here are the key principles to carry forward:
- Source of truth: Read metrics from
/proc/loadavgand/proc/meminfo— they are stable, locale-independent, and available on every Linux host - Float arithmetic: Use
bcfor floating-point threshold comparisons; Bash integer comparison (-gt) only works on whole numbers - Disk iteration: Use
df --output=pcent,targetto check every mounted filesystem, not just/ - Structured logging: Prefix every line with a UTC timestamp and severity level so logs are grep-friendly and ship cleanly to centralised logging systems
- Cooldown locks: Lock files checked with
find -mminprevent alert storms without changing the cron schedule - Composability: Exit with code
1when any alert fires so the script integrates with Nagios, Icinga, or other monitoring frameworks - Testing: Use
bash -nfor syntax checks,bash -xfor trace debugging, and aMOCK_MAILflag to validate the alert path on healthy hosts
Schedule the finished script via /etc/cron.d/ and your infrastructure will self-monitor continuously, alerting only when thresholds are meaningfully breached.
Frequently asked questions
Is the “Building System Health Check and Alert Scripts” lesson free?
Yes — the full text of “Building System Health Check and Alert Scripts” is free to read here on the web, and the DevOps Bootcamp course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the DevOps Bootcamp course, upgrade to CoddyKit PRO.
What will I learn in “Building System Health Check and Alert Scripts”?
Collect load, memory, and disk metrics and trigger threshold-based alerts from scheduled scripts. You practise DevOps Bootcamp with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start DevOps Bootcamp?
No prior experience is required. DevOps Bootcamp on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Building System Health Check and Alert Scripts” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this DevOps Bootcamp lesson?
Yes. Every DevOps Bootcamp lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Automating User and Group Provisioning
- Controlling systemd Services and Writing Unit Files
- Disk, Filesystem, and Mount Automation
- Building System Health Check and Alert Scripts