top shows the CPU mostly idle, no process uses more than a few percent, so the CPU can't be the problem. It must be the application, the database, or the disk. On bare metal that reasoning usually holds. On a virtual server it skips one number at the end of the CPU line in top: st, short for steal time.
Steal time is the share of time your virtual CPU wanted to run but the hypervisor (the layer that runs many virtual machines on one physical host) gave the physical core to another guest instead. When steal is high, your server is waiting for CPU it never received, and the usual per-process numbers will not show it.
Why low CPU usage doesn't mean the CPU is available
%CPU column looks harmless. A process can only accumulate CPU time when it actually gets CPU, so a starved process shows low usage exactly because it's starved. Requests still take longer, page loads stretch out, cron jobs overrun, and SSH typing can feel laggy, while top suggests the box is relaxed.
This is usually called the noisy neighbor problem. Hosts are often overcommitted, meaning more vCPUs are sold than physical cores exist. That's normal and usually fine because most guests are idle most of the time. It breaks down when several guests on the same host get busy at once.
Reading st in top and vmstat
top, the second summary line splits CPU time into categories. The last field is steal.
TEXT
# Illustrative output format (numbers are made up)
%Cpu(s): 8.0 us, 3.0 sy, 0.0 ni, 54.0 id, 1.0 wa, 0.0 hi, 0.0 si, 34.0 stus is user-space code, sy is kernel work, id is idle, wa is time waiting on I/O, and st is steal. Someone glancing at us and sy sees about 11% and moves on. The 34% steal says a third of the time your vCPUs wanted to run, they were not allowed to. Press 1 in top to see each vCPU separately, since steal often hits some cores more than others.
top refreshes every few seconds by default, and steal tends to come in bursts. vmstat gives a cleaner rolling view:
BASH
# Read-only: one sample per second, ten samples
vmstat 1 10
# Illustrative output format (numbers are made up)
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa st
# 6 0 0 812000 52000 940000 0 0 4 20 900 1500 9 3 50 1 37
# 5 0 0 811000 52000 940000 0 0 0 16 880 1450 8 3 52 0 37vmstat output, it's an average since boot. Read the rest together. The r column is the number of processes runnable or running. If r stays above your vCPU count while st is high, work is queuing for CPU that the host isn't delivering. Depending on your procps version, there may be an extra guest-time column after st.
One caveat: steal only shows up when the virtualization type reports it. Full VMs on KVM or Xen usually do. Container-based VPS plans may show st as 0 or a value that doesn't reflect your share. There, CPU limits are typically enforced with cgroups and show up as throttling instead, which is covered in Linux Kernel cgroups v2: How Modern Multi-Tenant Web Hosting Isolates CPU & Memory.
Telling steal apart from a real bottleneck inside the guest
st is high and r exceeds the vCPU count, the guest wants CPU and the host isn't providing it. Tuning the app further mostly moves the queue around.
If st is low but wa is high and the b column (processes blocked, usually on I/O) is non-zero, the bottleneck is storage. That can still be a neighbor problem at the disk layer, but it's a different conversation and a different set of tools, such as iostat.
If si and so (swap in and swap out) are non-zero during slowness, the server is short on memory and paging. CPU and disk numbers look odd as a side effect.
If us is near 100% across all vCPUs and st is low, you are using the CPU you paid for. That's an application or sizing problem, and the provider has nothing to fix.
If everything looks quiet, including low st, low wa, and an empty run queue, the slowness is probably a wait outside the CPU: database locks, a slow upstream API, DNS lookups, or PHP workers all busy waiting on something. Those need application-level tracing, not system counters.
As a rule of thumb, a few percent of steal now and then is normal background on shared hosts. Sustained double-digit steal that lines up with user-visible slowness is worth escalating. These are not official thresholds, and what's tolerable depends on the workload. Latency-sensitive services feel steal sooner than batch jobs.
On the support desk, this is where a lot of confusion comes from. The customer sees idle CPU, assumes the plugin or the database is at fault, and spends hours tuning things that were never the bottleneck. Reading one vmstat sample at the start usually saves that time.
Proving it over time before you open a ticket
top with high steal is easy to dismiss as a momentary spike. What makes the case is steal measured over time and correlated with the slowdowns.
BASH
# Per-vCPU breakdown, 1-second interval, 5 samples (sysstat package)
mpstat -P ALL 1 5
# Today's CPU history, including %steal, if sysstat collection is enabled
sar -u
# Same for a previous day; file path and naming vary by distro
sar -u -f /var/log/sysstat/sa15mpstat and sar both come from the sysstat package. sar only has history if the collector was already running, so install and enable it before you need it. The service setup and log directory differ between distros (Debian-based systems commonly use /var/log/sysstat, RHEL-based ones /var/log/sa).
When you contact the provider, include the time windows when slowness happened, the %steal values from sar for those windows, and a short vmstat 1 capture taken during an incident. Also note the vCPU count. That turns "my server feels slow" into data the provider can match against host-side metrics.
If you run the hypervisor yourself, for example on Proxmox, the same reasoning applies from the other side. High steal inside a guest means the host's physical cores are oversubscribed at that moment. Check host-level CPU load and which other guests are busy at the same time before touching the slow VM.
What actually fixes sustained steal
top looks quiet, read the st field and run vmstat 1 before touching the application. If steal is high and the run queue is backed up, collect sar history and take it to the provider. If steal is low, you've ruled out the host in under a minute and can move on to disk, memory, or the application with a clear conscience.