Your server’s swap fills up at 2 a.m., or a background job pins one core at 100 percent, and you don’t have Datadog, Netdata, or even htop installed on the box. Maybe it’s a hardened production host where installing packages needs a change request. Maybe it’s a $5 VPS you don’t want to burden with an agent process. Either way, you need to monitor CPU, RAM, and disk on a Linux server without third-party tools, using only what already ships on the machine.
The good news: every mainstream Linux distribution ships with everything you need. /proc, top, free, df, du, and vmstat come from the kernel and the procps/coreutils packages, both installed by default. You don’t need root to read most of them, and you don’t need network access to a package repo. This guide covers the exact commands, what their output actually means, and how to string them into a lightweight logging loop when a single snapshot isn’t enough.
How do you monitor CPU, RAM, and disk on a Linux server without third-party tools?
Four commands cover almost every case: top (or its batch mode) for a live process and load snapshot, free for memory, df/du for disk, and vmstat for a rolling view of CPU and I/O. All four ship as part of procps-ng or coreutils and are preinstalled on Debian, Ubuntu, RHEL, Fedora, Arch, and most Alpine images (busybox variants use slightly different flags). None require configuration, a daemon, or a config file, and none send data anywhere – the output stays in your terminal.
- CPU:
top, or reading/proc/statdirectly for scripting. - Memory:
free -h, or/proc/meminfofor the raw counters. - Disk:
df -hfor space,dufor what’s using it,df -ifor inode exhaustion. - Trends over time:
vmstat 1, or a short shell loop logging to a file (covered below).
The rest of this guide walks through each one, with the parts of the output that actually matter and the parts that are noise.
How do you check CPU usage without third-party tools like htop?
What top’s header actually tells you
Run top with no arguments and look at the first two lines before the process table.
top - 14:32:05 up 12 days, 3:41, 2 users, load average: 2.15, 1.88, 1.42
%Cpu(s): 34.2 us, 6.1 sy, 0.0 ni, 58.0 id, 1.2 wa, 0.0 hi, 0.5 si, 0.0 st
Load average is the average number of processes wanting a CPU, running or waiting on I/O, measured over 1, 5, and 15 minutes. On a 4-core box, a load average sustained above 4 means processes are queuing for CPU time. It’s not a percentage, and comparing it across machines with different core counts is meaningless unless you divide by core count first (nproc gives you that number).
The %Cpu(s) line breaks usage down: us (user processes), sy (kernel/system calls), id (idle), wa (waiting on disk I/O). A high wa with low us usually means a disk bottleneck, not a CPU one, so check the disk section below before assuming you need a bigger instance.
For a one-shot snapshot without the interactive UI, useful in scripts or cron, use batch mode:
top -b -n1 | head -20
Reading raw CPU numbers from /proc/stat
top gets its numbers from /proc/stat, and you can read the same source directly, which is useful for scripting or when top isn’t behaving as expected.
cat /proc/stat
cpu 3200142 8123 612044 9834211 128332 0 9541 0 0 0
The first line lists cumulative CPU ticks since boot, in this order: user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice. A single read only tells you totals since boot, not current load. Take two samples with a short delay and compute the difference to get real instant usage, which is exactly what the watch script later in this guide does.
For the fastest way to find which single process is responsible for a spike, without any scripting, use ps aux --sort=-%cpu | head.
That command names the offending process in seconds. This is a common pattern if you’re running AI coding agents on a remote server over SSH: a long, unattended agent loop can peg a core for hours before anyone notices, especially on a box nobody is watching interactively.
How do you check RAM usage with free and /proc/meminfo?
free -h is the fastest read:
free -h
total used free shared buff/cache available
Mem: 15Gi 4.2Gi 1.1Gi 210Mi 10Gi 10Gi
Swap: 2.0Gi 0B 2.0Gi
Ignore the free column. On Linux, unused memory gets used as page cache almost immediately after boot, so free looks artificially low within minutes of startup and stays that way. The number that matters is available: how much memory is actually usable by new processes without swapping, once reclaimable cache is accounted for. If available drops close to zero and swap usage climbs at the same time, that’s real memory pressure.
For the raw numbers, useful in scripts, /proc/meminfo has everything free derives its output from:
grep -E 'MemTotal|MemAvailable|SwapTotal|SwapFree' /proc/meminfo
MemAvailable is the kernel’s own estimate of usable memory, introduced in kernel 3.14, and is more accurate than computing free plus cache yourself, since it also accounts for reclaimable slab memory.
How do you monitor disk space and disk growth with df and du?
df -h shows space used per mounted filesystem:
df -h /
Watch the Use% column, not just the raw size. A filesystem can report plenty of free space in df -h and still be functionally full if it’s run out of inodes, which is common on filesystems with millions of small files, like mail queues or session caches. Check that separately:
df -i /
When df says a filesystem is full but you can’t find what’s using the space, du narrows it down. Run it one directory level at a time rather than recursively dumping everything at once:
du -h --max-depth=1 /var | sort -rh | head
Fresh deploys are a common trigger for disk filling up unexpectedly: log files, build artifacts, and package caches accumulate fast in the first few days. If you’ve just deployed a Laravel app with FrankenPHP on a VPS, storage/logs and Composer’s cache directory are usually the first places worth checking before anything more exotic.
One
dugotcha worth knowing: a deleted file still held open by a running process, a log a service is actively writing to, for example, keeps consuming disk space until that process closes the file handle or restarts.dfshows the space as used;dudoesn’t see the file at all because it’s already unlinked from the directory tree.lsof +L1lists open files with zero remaining links, which is the fastest way to confirm this is what’s happening.
How do you watch I/O and per-process load over time?
vmstat for system-wide throughput
vmstat gives a rolling view of CPU, memory, swap, and disk I/O in one place, which makes it good for spotting whether a slowdown is CPU-bound, memory-bound, or I/O-bound at a glance.
vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 1148920 92104 9821440 0 0 3 12 84 156 12 3 84 1 0
Ignore the first line of output; it’s an average since boot, not a real sample. r, processes waiting for CPU, sustained above your core count means CPU contention. si/so, swap in/out, above zero under load means you’re actively swapping, which is usually worse for performance than the RAM shortage that caused it. bi/bo are blocks read and written per second, useful for confirming disk I/O is the bottleneck a high wa figure in top hinted at.
iostat and pidstat, and why they might not exist yet
iostat and pidstat give more detail, per-device I/O and per-process CPU history, but ship in the sysstat package, which isn’t installed by default on every distribution. Some RHEL and CentOS images include it; a minimal Ubuntu or Alpine image usually doesn’t. Check before assuming it’s there:
command -v iostat || echo "not installed"
If it’s missing and installing a package is off the table, vmstat plus ps aux --sort=-%cpu covers the same ground for most troubleshooting: system-wide throughput from vmstat, the specific offending process from ps.
How do you turn these commands into a lightweight monitoring loop?
A single snapshot tells you what’s happening right now. It doesn’t tell you what happened at 3 a.m. before anyone was watching. A short shell script that samples on an interval and appends to a CSV solves that without installing anything.
#!/usr/bin/env bash
# resource-watch.sh - append a CPU/RAM/disk sample every 60s
INTERVAL=60
LOGFILE="/var/log/resource-watch.csv"
[ -f "$LOGFILE" ] || echo "timestamp,cpu_pct,mem_used_pct,disk_used_pct" > "$LOGFILE"
read_cpu() { awk '/^cpu /{for (i=2;i<=NF;i++) tot+=$i; print tot, $5}' /proc/stat; }
while true; do
read prev_total prev_idle < <(read_cpu)
sleep "$INTERVAL"
read total idle < <(read_cpu)
cpu_pct=$(awk -v t="$total" -v pt="$prev_total" -v i="$idle" -v pi="$prev_idle" \
'BEGIN { dt = t - pt; di = i - pi; printf "%.1f", (dt - di) / dt * 100 }')
mem_pct=$(free | awk '/^Mem:/{printf "%.1f", ($2-$7)/$2*100}')
disk_pct=$(df --output=pcent / | tail -1 | tr -dc '0-9.')
echo "$(date -Iseconds),${cpu_pct},${mem_pct},${disk_pct}" | tee -a "$LOGFILE"
done
This samples CPU by reading /proc/stat before and after the sleep interval and computing the delta, the same method top uses internally. A single raw read only gives you the average since boot, not current load. Run it in a tmux or screen session, or wrap it in a small systemd unit if you want it to survive reboots.
For periodic snapshots without a long-running process, cron is simpler than a background loop. vmstat 1 2 conveniently does the two-sample delta for you internally, so a cron-friendly version doesn’t need the manual /proc/stat math:
#!/usr/bin/env bash
# resource-snapshot.sh - one CSV line, safe to run from cron
read _ us sy idl wa _ < <(vmstat 1 2 | tail -1 | awk '{print $1,$13,$14,$15,$16,$17}')
cpu_pct=$((100 - idl))
mem_pct=$(free | awk '/^Mem:/{printf "%.0f", ($2-$7)/$2*100}')
disk_pct=$(df --output=pcent / | tail -1 | tr -dc '0-9')
echo "$(date -Iseconds),${cpu_pct},${mem_pct},${disk_pct}"
*/5 * * * * /usr/local/bin/resource-snapshot.sh >> /var/log/resource-watch.csv 2>&1
When do manual Linux server monitoring commands stop being enough?
These commands are the right call for a single server, a one-off investigation, or an environment where installing anything needs sign-off. They have real limits worth naming honestly:
- No history beyond your log file’s retention. If you didn’t have the watch script running, the 3 a.m. spike is gone.
- No alerting. A CSV file doesn’t page anyone at 3 a.m., you have to go looking for it yourself.
- No cross-server view. Each box is its own SSH session and its own log file; there’s no dashboard across a fleet.
- The observer effect. A script left running in a forgotten tmux session is itself something you now have to remember to maintain.
For one server you check occasionally, that’s a fair trade for zero install footprint. Once you’re running more than a couple of boxes, or you want a slowdown to page you instead of waiting to be noticed, a push-based agent that keeps history and alerting outside the box itself is worth the tradeoff. That’s what termique’s built-in server monitoring does: a small agent posts CPU, RAM, and disk metrics every 30 seconds, and termique keeps the history and surfaces alerts without you needing to SSH in and run a single command.
termique’s server monitoring is opt-in per host and installs with a single script, no dashboard account or extra infrastructure to run yourself. If you manage more than one server and are tired of SSH-ing in just to run
free -h, it’s worth five minutes to set up.