You ran a benchmark, got a wall of numbers, and now have to decide whether your VPS is actually underpowered or just momentarily busy. Most benchmark guides stop at “here is how to run the tool.” The harder skill is interpretation: a 2.0 load average means something completely different on a 2-core instance than on a 16-core one, and CPU steal time can silently throttle you while every local process looks healthy. This guide focuses on reading the output correctly, not on generating it.
If you are still choosing an instance size or comparing plans, start with the capacity planning approach on our main VPS plans and specifications page and come back here once you have numbers to read.
Prerequisites
- A running VPS with SSH access and root or sudo privileges
sysstatinstalled forsarandiostat:apt install sysstatordnf install sysstat- A benchmark run you already completed, or the ability to generate one now
- Knowledge of how many vCPUs the instance advertises (
nproc)
Step 1: Establish the Baseline Before You Measure Anything
Record the hardware topology first, because every ratio you compute later depends on it. Run this and save the output:
nproc
lscpu | grep -E 'Model name|CPU\(s\)|MHz|Hypervisor'
free -h
cat /proc/pressure/cpu
cat /proc/pressure/io
The /proc/pressure/* files give you PSI (Pressure Stall Information), which is far more honest than load average. Each line reports the percentage of time tasks were stalled waiting for that resource, averaged over 10, 60, and 300 seconds. some avg10=45.20 on the CPU line means that for 45% of the last ten seconds, at least one task could not run because the CPU was unavailable. That is a real saturation signal.
Step 2: Decode Load Average Relative to vCPU Count
Load average counts runnable plus uninterruptible-sleep (D-state) tasks. The only meaningful reading is load divided by vCPU count:
uptime
# normalize manually:
python3 -c "import os; l=os.getloadavg()[0]; c=os.cpu_count(); print(f'load={l:.2f} cpus={c} ratio={l/c:.2f}')"
Interpretation guide: a ratio below 0.7 means headroom. Between 0.7 and 1.0 means you are at capacity but not queued. Above 1.0 means work is waiting; above 2.0 sustained means genuine CPU starvation and a resize is justified. A rising ratio accompanied by flat CPU utilization is the classic signature of I/O-bound D-state accumulation, not a CPU shortage.
Step 3: Separate CPU Steal from Genuine Local Load
CPU steal (%st) is time your virtual CPU was runnable but the physical host scheduled someone else. This is the number that distinguishes “my code is slow” from “my neighbor is noisy.” Watch it live:
vmstat 2 10
# columns: us sy id wa st
mpstat -P ALL 2 5
Steal is reported in the st column of vmstat. Thresholds that matter in practice:
| %st sustained | Meaning | Action |
|---|---|---|
| 0-1% | Normal, negligible | Nothing |
| 2-5% | Mild host contention | Monitor; check peak hours |
| 5-15% | Real throttling; latency spikes likely | Open a ticket or move to a dedicated-vCPU plan |
| >15% | Severe oversubscription | Migrate off the host |
A useful cross-check: if %st is high but %us (user) and %sy (system) are low, your instance is idle-but-blocked. No amount of application tuning will fix that; only a different plan or host will.
Step 4: Read iowait Without Overreacting
iowait (wa) is not a disk-speed metric on its own. It is the CPU idle time during which at least one I/O was outstanding. On a fast NVMe instance you can still see high wa if a slow network filesystem or a blocking device is in play. Confirm with device-level stats, where the real signal lives in the await and queue columns:
iostat -x 2 5
# look at: r/s w/s rkB/s wkB/s await aqu-sz %util
Read await (average milliseconds per request) and aqu-sz (average queue depth) together. High await with aqu-sz near zero is usually a single slow request, not saturation. High await with aqu-sz consistently above 1 means the device is the bottleneck. %util near 100% on a single queue is meaningful, but on modern NVMe with deep queues it can read misleadingly high while the drive still has throughput in reserve.
Step 5: Correlate with Historical Data
A single snapshot proves little. sysstat retains a rolling history so you can check whether today is anomalous:
sar -u 1 5 # live CPU incl. %steal
sar -u -f /var/log/sysstat/sa$(date +%d) # today's history
sar -q # load average history
sar -r # memory over time
Compare the same hour on a busy day against a quiet one. If steal and load both climb together at a predictable time, you have a scheduled-job conflict you can fix yourself by rescheduling cron. If steal climbs while your own traffic is flat, the contention is external.
Verification Step
After you finish interpreting, write the numbers down and set a threshold check. Confirm the data is internally consistent before you act on it:
# All three should roughly satisfy: us + sy + id + wa + st = 100
mpstat 1 3
# Confirm your ratio-based conclusion
python3 -c "import os; l=os.getloadavg(); c=os.cpu_count(); [print(f'{x:.2f}s load -> ratio {x/c:.2f}') for x in l]"
# Confirm PSI agrees with the load-based read
cat /proc/pressure/cpu | head -1
If load ratio, PSI CPU some avg60, and %st all point the same direction, your conclusion is safe. If they disagree, trust PSI and steal over load average.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
vmstat not found | Missing procps | apt install procps |
No st column output | Older vmstat build | Use mpstat -P ALL instead |
High wa with idle disk | Blocking network FS or slow device | Identify mount with mount; move data local |
| High steal, low CPU usage | Noisy-neighbor host contention | Escalate to provider or resize |
| PSI files missing | Kernel without CONFIG_PSI | Fall back to sar history |
| Load spikes at fixed times | Overlapping cron jobs | Stagger schedules in /etc/cron.d |
With these readings in hand you can make a defensible decision about resizing, splitting workloads, or tuning the application. Numbers without normalization are noise; normalized numbers with a PSI cross-check are an argument you can take to your provider.


Leave a Reply
You must be logged in to post a comment.