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 Linux Command Line & Bash Scripting Mastery lesson on CoddyKit. This is 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 Linux Command Line & Bash Scripting Mastery learning path, and your progress syncs across the web and the CoddyKit app. The Linux Command Line & Bash Scripting Mastery course includes 4 lessons in total.
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.
Learn Bash with an AI tutor — free
Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.
- Courses
- 22
- Lessons
- 88
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. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Linux Command Line & Bash Scripting Mastery course, upgrade to CoddyKit PRO. The Linux Command Line & Bash Scripting Mastery course includes 4 lessons in total.
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 Linux Command Line & Bash Scripting Mastery 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 Linux Command Line & Bash Scripting Mastery?
No prior experience is required. Linux Command Line & Bash Scripting Mastery on CoddyKit is structured for beginners through advanced learners, so you can start here or from the beginning and move at your own pace. This is lesson 4 of 4.
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 Linux Command Line & Bash Scripting Mastery lesson?
Yes. Every Linux Command Line & Bash Scripting Mastery 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