Knowing what your server or workstation is doing at any given moment — CPU load, memory pressure, disk space, network throughput — is one of the most practical things you can script in Bash. You don’t need a heavyweight monitoring platform to catch a runaway process or a disk filling up; a well-written Bash script checking the right files and commands can alert you long before things get serious.
This guide walks through the core tools and techniques for monitoring system resources directly from the shell, and how to turn that into real automation.
Where System Resource Data Lives
On Linux, most system state is exposed through the /proc virtual filesystem — a set of files that don’t represent real data on disk, but live, kernel-generated information updated in real time. Understanding /proc is the foundation of resource monitoring in Bash, because it’s what tools like top, free, and ps read from under the hood.
Key files:
/proc/stat— CPU usage statistics/proc/meminfo— memory usage details/proc/loadavg— system load averages/proc/diskstats— disk I/O statistics/proc/net/dev— network interface statistics/proc/<pid>/status— per-process resource info
Monitoring CPU Usage
Using top in Batch Mode
top -bn1 | head -20
-b runs in batch mode (non-interactive, suitable for scripts), and -n1 limits it to a single iteration.
Extracting CPU Usage Percentage
#!/bin/bash
cpu_idle=$(top -bn1 | grep "Cpu(s)" | awk -F'id,' -v prefix="" '{ split($1, vs, ","); v=vs[length(vs)]; sub("%", "", v); print v }')
cpu_usage=$(echo "100 - $cpu_idle" | bc)
echo "CPU Usage: ${cpu_usage}%"
Reading Load Average Directly
#!/bin/bash
read one five fifteen rest < /proc/loadavg
echo "Load average (1m, 5m, 15m): $one, $five, $fifteen"
Load average represents the number of processes waiting for CPU time, averaged over the last 1, 5, and 15 minutes. On a system with 4 CPU cores, a load average consistently above 4 generally indicates the CPU is a bottleneck.
Monitoring Memory Usage
Using free
free -h
Parsing Memory Stats in a Script
#!/bin/bash
read total used free shared buff_cache available < <(free -m | awk '/^Mem:/ {print $2, $3, $4, $5, $6, $7}')
usage_percent=$(( used * 100 / total ))
echo "Memory used: ${used}MB / ${total}MB (${usage_percent}%)"
if (( usage_percent > 90 )); then
echo "WARNING: High memory usage!"
fi
Reading Directly from /proc/meminfo
#!/bin/bash
total=$(awk '/MemTotal/ {print $2}' /proc/meminfo)
available=$(awk '/MemAvailable/ {print $2}' /proc/meminfo)
used=$((total - available))
percent=$((used * 100 / total))
echo "Memory usage: ${percent}%"
MemAvailable (rather than MemFree) is the more accurate figure for “how much memory can actually be given to a new process,” since it accounts for reclaimable cache and buffers.
Monitoring Disk Usage
# Overall disk usage per filesystem
df -h
# Usage of a specific directory
du -sh /var/log
# Alert if any filesystem exceeds a threshold
#!/bin/bash
THRESHOLD=85
df -h --output=pcent,target | tail -n +2 | while read -r usage mount; do
pct=${usage%\%}
if (( pct >= THRESHOLD )); then
echo "WARNING: $mount is at ${pct}% capacity"
fi
done
Finding the Largest Directories Quickly
du -h --max-depth=1 /var | sort -rh | head -10
This is one of the most useful one-liners for tracking down what’s eating disk space — it lists top-level subdirectories sorted by size, largest first.
Monitoring Network Usage
# Snapshot of network interface statistics
cat /proc/net/dev
# Calculate throughput over an interval
#!/bin/bash
INTERFACE="eth0"
INTERVAL=1
rx1=$(cat /sys/class/net/$INTERFACE/statistics/rx_bytes)
tx1=$(cat /sys/class/net/$INTERFACE/statistics/tx_bytes)
sleep "$INTERVAL"
rx2=$(cat /sys/class/net/$INTERFACE/statistics/rx_bytes)
tx2=$(cat /sys/class/net/$INTERFACE/statistics/tx_bytes)
rx_rate=$(( (rx2 - rx1) / INTERVAL / 1024 ))
tx_rate=$(( (tx2 - tx1) / INTERVAL / 1024 ))
echo "Download: ${rx_rate} KB/s | Upload: ${tx_rate} KB/s"
This works by reading the cumulative byte counters exposed by the kernel for each network interface, taking two snapshots a fixed interval apart, and computing the difference to get a rate.
Monitoring Individual Processes
# Top 5 processes by CPU usage
ps aux --sort=-%cpu | head -6
# Top 5 processes by memory usage
ps aux --sort=-%mem | head -6
# Monitor a specific process by PID
pid=1234
cat /proc/$pid/status | grep -E "VmRSS|State"
Building a Full Monitoring Script
#!/bin/bash
# system_monitor.sh — simple resource monitor with alerting
CPU_THRESHOLD=85
MEM_THRESHOLD=90
DISK_THRESHOLD=85
LOGFILE="/var/log/system_monitor.log"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOGFILE"
}
check_cpu() {
local idle usage
idle=$(top -bn1 | grep "Cpu(s)" | awk -F',' '{print $4}' | awk '{print $1}')
usage=$(echo "100 - $idle" | bc | cut -d'.' -f1)
if (( usage >= CPU_THRESHOLD )); then
log "ALERT: CPU usage at ${usage}%"
fi
}
check_memory() {
local total available used percent
total=$(awk '/MemTotal/ {print $2}' /proc/meminfo)
available=$(awk '/MemAvailable/ {print $2}' /proc/meminfo)
used=$((total - available))
percent=$((used * 100 / total))
if (( percent >= MEM_THRESHOLD )); then
log "ALERT: Memory usage at ${percent}%"
fi
}
check_disk() {
df -h --output=pcent,target | tail -n +2 | while read -r usage mount; do
pct=${usage%\%}
if (( pct >= DISK_THRESHOLD )); then
log "ALERT: $mount at ${pct}% disk usage"
fi
done
}
check_cpu
check_memory
check_disk
How it works internally: each check_* function independently reads the relevant kernel-exposed data, computes a percentage, and appends a timestamped line to a log file only if the value crosses its threshold. Running this via cron every few minutes gives you a lightweight, dependency-free monitoring layer.
Automation: Scheduling and Alerting
# Add to crontab to run every 5 minutes
*/5 * * * * /usr/local/bin/system_monitor.sh
To go further, pipe alerts into mail, a Slack webhook via curl, or a Telegram bot API call, so you get notified without needing to check the log file manually:
if (( percent >= MEM_THRESHOLD )); then
curl -s -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"Memory alert: ${percent}% used on $(hostname)\"}" \
"$SLACK_WEBHOOK_URL"
fi
Best Practices
- Prefer reading
/procfiles directly over parsingtop/psoutput where possible — it’s faster and less fragile against formatting changes across distributions. - Always set sensible, environment-specific thresholds — 85% disk usage might be fine on a log-rotation-heavy server but critical on a small VPS.
- Log historical data, not just current snapshots, so you can identify trends (a slow memory leak, a gradually filling disk) rather than only reacting to sudden spikes.
- Keep monitoring scripts lightweight — a monitoring script that itself consumes significant CPU or memory defeats the purpose.
Security Considerations
- Monitoring scripts often run with elevated permissions (cron as root) to read all process info — restrict script file permissions (
chmod 700) so they can’t be tampered with. - Sanitize any hostname or dynamic values inserted into webhook payloads or shell commands to avoid injection if that data could ever be influenced by untrusted input.
- Avoid storing webhook URLs or API tokens in plaintext inside the script itself; use environment variables or a restricted-permission config file instead.
Optimization Tips
- Batch multiple
/procreads together rather than spawning many separate subprocesses (awk,grep) per check — each subprocess has overhead. - For very frequent monitoring (sub-second intervals), avoid
topentirely and read/proc/statdirectly, computing CPU usage deltas yourself, sincetopitself carries noticeable overhead when run repeatedly. - Use
vmstat 1 5for a lightweight, built-in way to sample CPU/memory/IO over several intervals without writing custom delta logic.
Troubleshooting Common Issues
Problem: CPU usage numbers look wrong or inconsistent. Different tools calculate CPU percentage differently (single-sample vs. delta-based). For accuracy, always compute usage as a difference between two /proc/stat snapshots rather than trusting a single instantaneous reading.
Problem: MemFree looks alarmingly low even though the system feels fine. Linux aggressively uses free memory for disk caching. Use MemAvailable instead of MemFree — it already accounts for reclaimable cache.
Problem: Disk usage percentage doesn’t match what du reports for a directory. df reports filesystem-level usage (including reserved blocks and other directories on the same mount), while du reports usage for the specific directory tree you pointed it at. They’re answering different questions.
Problem: The monitoring script doesn’t run under cron even though it works manually. Cron’s environment differs from an interactive shell — use absolute paths for all commands and files, and avoid relying on aliases or interactive-shell-only configuration.
Common Mistakes
- Relying solely on
MemFreeinstead ofMemAvailablefor memory alerts, causing false positives. - Setting the same alert thresholds across very different servers without adjusting for their actual workload.
- Forgetting to test scripts under cron’s minimal environment before scheduling them.
- Writing monitoring scripts that themselves consume excessive resources due to inefficient subprocess spawning.
- Not rotating or capping the monitoring log file, letting it grow unbounded over time.
Frequently Asked Questions
What’s the difference between load average and CPU usage percentage? CPU usage percentage reflects how busy the CPU is right now. Load average reflects the number of processes waiting for CPU (or I/O) time, averaged over a period — a high load average with low CPU usage often points to an I/O bottleneck rather than a CPU one.
Can Bash monitor resources without any external tools? Yes, almost entirely — /proc files provide raw CPU, memory, disk, and network data readable with pure Bash and awk, without needing top, free, or third-party utilities.
How often should a monitoring script run? It depends on the use case — every 1–5 minutes via cron is typical for general alerting; sub-minute intervals usually call for a purpose-built monitoring daemon rather than a repeatedly-scheduled script.
Is Bash suitable for production-grade monitoring, or should I use a dedicated tool? Bash scripts are excellent for lightweight, dependency-free checks and quick alerting, but for full historical dashboards, anomaly detection, and multi-host visibility, dedicated tools like Prometheus, Grafana, or Nagios are generally a better long-term investment.
Summary
Bash gives you direct access to the same real-time data that graphical monitoring tools rely on, all through simple, readable files in /proc and a handful of standard commands like top, free, df, and ps. Building a lightweight monitoring script — checking CPU, memory, disk, and network, logging results, and alerting on thresholds — is a practical, low-overhead way to catch problems on a server before they become outages.
References
- GNU Coreutils Manual: https://www.gnu.org/software/coreutils/manual/coreutils.html
- Linux Kernel Documentation — The /proc Filesystem: https://www.kernel.org/doc/html/latest/filesystems/proc.html
- GNU Bash Manual: https://www.gnu.org/software/bash/manual/bash.html
- man7.org proc(5): https://man7.org/linux/man-pages/man5/proc.5.html
