What VPS Virtual Private Server Prices Actually Buy: Specs Normalized, Steal Measured and Throttles Found

  • Idle sample: vmstat 1 30 | awk 'NR>2 {s+=$16; n++} END {print "avg steal "s/n"%"}'
  • Loaded sample: in one terminal run stress-ng --cpu $(nproc) --timeout 180s, in another run the same vmstat one-liner.
  • Interval-accurate check: perf stat -e task-clock,context-switches,cpu-migrations sleep 20 — if task-clock is well below wall time × vCPUs, scheduling is lossy.
  • Pinned single-core test: taskset -c 0 stress-ng --cpu 1 --cpu-method matrixprod --timeout 60s, then repeat with --taskset -c 1 and so on, to see whether one vCPU is chronically starved.
Idle stealLoaded stealInterpretation
< 0.5%< 2%Healthy node, cores effectively dedicated
< 0.5%5–10%Busy neighbours; expect tail latency spikes
> 2%> 15%Oversold; CPU-bound workloads will suffer
Any> 30% on one coreCore-specific contention; report to provider

A single sustained steal number is not enough. Run the loaded test for at least three minutes so the hypervisor scheduler’s periodic balancing shows up if it exists.

Phase 3: Disk IOPS, latency, and the throttle ceiling

The critical question is not peak IOPS but the point at which the provider’s throttle clamps your throughput. That shows up as a sudden latency step, not a gradual slowdown. Run a stepped queue-depth sweep:

fio --name=sweep --ioengine=io_uring --direct=1 --rw=randread --bs=4k --iodepth=1,4,8,16,32,64 --time_based --runtime=45 --size=4G --filename=/tmp/io.test --output-format=json --output=/tmp/fio-read.json

Then a mixed write test, which is closer to real database behaviour: --rw=randrw --rwmixread=70 --bs=8k. Parse the JSON for iops and clat_ns.percentile["99.000000"]. A healthy NVMe-backed instance holds p99 latency roughly flat up to queue depth 32. If p99 jumps from 0.4 ms at depth 8 to 12 ms at depth 16 while IOPS stop increasing, you have found the throttle.

  • Compare sequential read bandwidth separately: --rw=read --bs=1M --iodepth=16. Bandwidth ceilings are often enforced separately from IOPS ceilings.
  • Watch iostat -x 1 during the test; a rising aqu-sz with flat r/s confirms throttling rather than device saturation.
  • Check whether the throttle follows the disk or the cgroup: containerise the same test inside a Docker container with --device-write-iops unset and compare numbers.

Write amplification on a fresh disk is misleadingly low. If your workload is write-heavy, pre-fill the file with --fill_device or run the test against a database you have actually populated before drawing conclusions.

Phase 4: Network throughput, latency, and jitter

Three separate properties matter: raw bandwidth, round-trip latency, and variance. Test each against a location that matches your user base, not against a nearby datacentre.

iperf3 -c <peer> -t 30 -P 4 --bidir gives achieved throughput per stream. Compare against the advertised port speed; a plan sold as 1 Gbps that peaks at 350 Mbps has either a shared uplink or a per-instance shaping rule. Run mtr -rwc 100 -i 0.2 <target> and read the loss and StdDev columns per hop. Loss that appears at hop 3 and disappears by hop 6 is usually ICMP deprioritisation, not real packet loss; loss that persists to the final hop is real.

For jitter, which matters more than averages for anything interactive, sample TCP RTT directly: ss -tin dst <ip> exposes smoothed RTT and RTT variance (rtt and rttvar). A link with 20 ms mean and 2 ms variance is far better than 12 ms mean with 15 ms variance for websockets or SSH sessions.

TestCommandPass criterion
Bandwidthiperf3 -P 4 –bidir≥ 80% of advertised port
Path lossmtr -rwc 1000% at final hop
Jitterss -tin / ping -i 0.2StdDev < 20% of mean RTT
TLS handshakeopenssl s_time -connect host:443 -new -time 10Handshakes/s stable across runs

Phase 5: Realistic stack test, then decide

Synthetic tests tell you the hardware envelope. The final decision should come from a representative stack. Deploy nginx with a small dynamic backend, warm the cache, then run wrk -t4 -c200 -d60s --latency http://<ip>/test.php. Read the p99 rather than requests per second: p99 is where steal and I/O throttling surface.

Repeat the whole sequence twice, an hour apart. Providers whose numbers move 40% between runs are unsuitable for latency-sensitive work regardless of headline specs. If you plan to keep a single instance under sustained load rather than burst, the isolation guarantees discussed in the comparison of VPS and dedicated server resource models explain why identical specs can behave so differently.

Score each candidate on four numbers: loaded steal percentage, p99 disk latency at your working queue depth, achieved bidirectional bandwidth, and p99 application latency under a realistic connection count. Those four predict production behaviour far better than any single benchmark score.

Two VPS plans can carry identical vCPU, RAM, and NVMe figures and still deliver throughput that differs by a factor of three. The gap almost always comes from three measurable quantities: CPU steal time from the hypervisor scheduler, disk IOPS ceilings enforced by per-device throttles, and network jitter between your instance and its users. This article gives a testing procedure that isolates each of those three, with copy-paste commands, so you can decide from data rather than from a specs table.

Run these tests on a fresh instance with no production traffic, and repeat each one at the same time of day across candidates — neighbourhood load is time-dependent. The premium cloud solutions platform used here exposes standard KVM paravirtualised devices, so every command below applies unchanged to any comparable provider.

Phase 1: Baseline the box before testing anything

Record the environment so results are reproducible and so you can spot a provider that silently changes hardware mid-test.

lscpu; free -m; df -hT; uname -r; cat /sys/block/*/queue/scheduler

Then install the toolchain: apt install -y fio sysstat iperf3 mtr-tiny stress-ng. Note the filesystem and mount options in /proc/mounts, because noatime, discard, and barrier settings change disk numbers substantially.

Phase 2: CPU steal — what you actually bought

Steal time is the fraction of wall-clock time your vCPU was runnable but not scheduled. Measure it idle and under load, because oversold nodes show steal only when neighbours are busy.

  • Idle sample: vmstat 1 30 | awk 'NR>2 {s+=$16; n++} END {print "avg steal "s/n"%"}'
  • Loaded sample: in one terminal run stress-ng --cpu $(nproc) --timeout 180s, in another run the same vmstat one-liner.
  • Interval-accurate check: perf stat -e task-clock,context-switches,cpu-migrations sleep 20 — if task-clock is well below wall time × vCPUs, scheduling is lossy.
  • Pinned single-core test: taskset -c 0 stress-ng --cpu 1 --cpu-method matrixprod --timeout 60s, then repeat with --taskset -c 1 and so on, to see whether one vCPU is chronically starved.
Idle stealLoaded stealInterpretation
< 0.5%< 2%Healthy node, cores effectively dedicated
< 0.5%5–10%Busy neighbours; expect tail latency spikes
> 2%> 15%Oversold; CPU-bound workloads will suffer
Any> 30% on one coreCore-specific contention; report to provider

A single sustained steal number is not enough. Run the loaded test for at least three minutes so the hypervisor scheduler’s periodic balancing shows up if it exists.

Phase 3: Disk IOPS, latency, and the throttle ceiling

The critical question is not peak IOPS but the point at which the provider’s throttle clamps your throughput. That shows up as a sudden latency step, not a gradual slowdown. Run a stepped queue-depth sweep:

fio --name=sweep --ioengine=io_uring --direct=1 --rw=randread --bs=4k --iodepth=1,4,8,16,32,64 --time_based --runtime=45 --size=4G --filename=/tmp/io.test --output-format=json --output=/tmp/fio-read.json

Then a mixed write test, which is closer to real database behaviour: --rw=randrw --rwmixread=70 --bs=8k. Parse the JSON for iops and clat_ns.percentile["99.000000"]. A healthy NVMe-backed instance holds p99 latency roughly flat up to queue depth 32. If p99 jumps from 0.4 ms at depth 8 to 12 ms at depth 16 while IOPS stop increasing, you have found the throttle.

  • Compare sequential read bandwidth separately: --rw=read --bs=1M --iodepth=16. Bandwidth ceilings are often enforced separately from IOPS ceilings.
  • Watch iostat -x 1 during the test; a rising aqu-sz with flat r/s confirms throttling rather than device saturation.
  • Check whether the throttle follows the disk or the cgroup: containerise the same test inside a Docker container with --device-write-iops unset and compare numbers.

Write amplification on a fresh disk is misleadingly low. If your workload is write-heavy, pre-fill the file with --fill_device or run the test against a database you have actually populated before drawing conclusions.

Phase 4: Network throughput, latency, and jitter

Three separate properties matter: raw bandwidth, round-trip latency, and variance. Test each against a location that matches your user base, not against a nearby datacentre.

iperf3 -c <peer> -t 30 -P 4 --bidir gives achieved throughput per stream. Compare against the advertised port speed; a plan sold as 1 Gbps that peaks at 350 Mbps has either a shared uplink or a per-instance shaping rule. Run mtr -rwc 100 -i 0.2 <target> and read the loss and StdDev columns per hop. Loss that appears at hop 3 and disappears by hop 6 is usually ICMP deprioritisation, not real packet loss; loss that persists to the final hop is real.

For jitter, which matters more than averages for anything interactive, sample TCP RTT directly: ss -tin dst <ip> exposes smoothed RTT and RTT variance (rtt and rttvar). A link with 20 ms mean and 2 ms variance is far better than 12 ms mean with 15 ms variance for websockets or SSH sessions.

TestCommandPass criterion
Bandwidthiperf3 -P 4 –bidir≥ 80% of advertised port
Path lossmtr -rwc 1000% at final hop
Jitterss -tin / ping -i 0.2StdDev < 20% of mean RTT
TLS handshakeopenssl s_time -connect host:443 -new -time 10Handshakes/s stable across runs

Phase 5: Realistic stack test, then decide

Synthetic tests tell you the hardware envelope. The final decision should come from a representative stack. Deploy nginx with a small dynamic backend, warm the cache, then run wrk -t4 -c200 -d60s --latency http://<ip>/test.php. Read the p99 rather than requests per second: p99 is where steal and I/O throttling surface.

Repeat the whole sequence twice, an hour apart. Providers whose numbers move 40% between runs are unsuitable for latency-sensitive work regardless of headline specs. If you plan to keep a single instance under sustained load rather than burst, the isolation guarantees discussed in the comparison of VPS and dedicated server resource models explain why identical specs can behave so differently.

Score each candidate on four numbers: loaded steal percentage, p99 disk latency at your working queue depth, achieved bidirectional bandwidth, and p99 application latency under a realistic connection count. Those four predict production behaviour far better than any single benchmark score.

Leave a Reply