0Pricing
DevOps Bootcamp · Lesson

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 — outputs 1 (true) or 0 (false) from a comparison expression
  • awk — 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}%"
fi

Collecting Memory Metrics

/proc/meminfo is the authoritative source for memory statistics on Linux. Key fields:

  • MemTotal — total physical RAM in kB
  • MemAvailable — estimated kB available for new allocations without swapping (better than MemFree)

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
done

Structured 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 stderr for ALERT and to stdout for 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 mail with a curl call 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 main function orchestrates the calls
  • Exit code 1 if any alert fired, 0 otherwise — 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"
}

main

Scheduling 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 — $PATH is 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>&1

Preventing 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)"
fi

Testing and Validating Your Health Check Script

Before deploying, validate the script in three ways:

  • Syntax check: bash -n healthcheck.sh catches parse errors without executing
  • Trace mode: bash -x healthcheck.sh prints 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}%"
fi

Knowledge 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/loadavg and /proc/meminfo — they are stable, locale-independent, and available on every Linux host
  • Float arithmetic: Use bc for floating-point threshold comparisons; Bash integer comparison (-gt) only works on whole numbers
  • Disk iteration: Use df --output=pcent,target to 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 -mmin prevent alert storms without changing the cron schedule
  • Composability: Exit with code 1 when any alert fires so the script integrates with Nagios, Icinga, or other monitoring frameworks
  • Testing: Use bash -n for syntax checks, bash -x for trace debugging, and a MOCK_MAIL flag 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

  1. Automating User and Group Provisioning
  2. Controlling systemd Services and Writing Unit Files
  3. Disk, Filesystem, and Mount Automation
  4. Building System Health Check and Alert Scripts
← Back to DevOps Bootcamp