Cost per gigabyte of RAM is the wrong metric for a VPS, because the RAM itself is not what you are buying. What you are buying is a share of the host’s coherence domain: the last-level cache, the memory controller, and the physical socket the scheduler is willing to keep your vCPUs on. On a 2-socket host with 64+ cores, a 2-vCPU guest can be placed across a socket boundary and pay a cache-line transfer on every shared-memory access — a cost that never appears in top but shows up as a 30–50% throughput difference against an identical guest placed inside one NUMA node.
This page covers how to detect that, and which spec axes actually predict performance. For the plan-tier breakdown that maps these axes to price bands, see our VPS plan sizing guide by workload.
The Specs That Predict Nothing
| Advertised spec | Predictive power | Why |
|---|---|---|
| vCPU count | Low | Share of a core, a full core and a pinned core all read “2 vCPU” |
| RAM size | Medium | Real, but usually the easiest axis to satisfy |
| Disk size | None | Unrelated to IOPS, which depends on backing store and tier |
| “NVMe” | Low | Local NVMe, network NVMe and tiered NVMe all use the same word |
| Bandwidth “unmetered” | Low | Port speed and traffic allowance are separate quantities |
| CPU model string | High | Generation determines IPC, AVX-512 availability and memory speed |
| Guaranteed / dedicated CPU | High | Changes the scheduler behaviour under host contention |
| Provider’s region and upstream | High | Determines RTT to your users and whether the path is direct |
Axis 1: Is the vCPU a Core or a Share?
Three products look identical on an order page. The difference shows up only under contention, so measure it by loading all your vCPUs and watching steal and frequency together.
# Load every vCPU for 120s
nproc --all
sudo apt install -y stress-ng sysstat
stress-ng --cpu $(nproc) --cpu-method matrixprod --timeout 120s &
# In another shell
vmstat 5 24 | awk 'NR>2{print $13,$14,$15,$16}'
grep -m1 'model name' /proc/cpuinfo; lscpu | grep -E 'MHz|Socket|NUMA'
| Steal (st) | us+sy | Interpretation |
|---|---|---|
| 0–2% | ~200% on 2 vCPU | Dedicated cores or a very quiet host |
| 3–8% | ~190% | Oversubscribed but well managed — usually fine |
| 10–25% | varies | Active contention; latency will be unpredictable |
| >25% | varies | Not a shared-core product you can run a database on |
Steal is reported by the hypervisor, so it is trustworthy when it is high and uninformative when it is zero — some hypervisors simply do not expose it. Cross-check with /proc/stat on the guest: if the sum of user + system + idle does not add up to wall-clock time × vCPUs, the missing time is stolen regardless of what st says.
Axis 2: Single-Core Speed Is Usually the Binding Constraint
Most VPS workloads — PHP-FPM workers, MySQL query execution, Node.js event loops, Redis command processing — are single-threaded per request. Four slow cores beat two fast cores only when you have real concurrency; below roughly 50 requests per second, single-core speed dominates response time.
sudo sysbench cpu --threads=1 --cpu-max-prime=20000 --time=60 run | grep 'events per second'
sysbench cpu --threads=1 --cpu-max-prime=20000 --time=60 run | grep 'events per second'
| Events/sec (1 thread, prime 20000) | CPU class | Typical price band |
|---|---|---|
| 2,000–2,400 | Older Xeon E5 / EPYC 7002, often oversubscribed | Entry, $5–8 |
| 2,600–3,200 | Xeon Scalable Gold, EPYC 7003 | Mid, $10–25 |
| 3,400–4,200 | Recent EPYC 9004 / 9005, Ryzen dedicated | Upper mid, $25–50 |
| 4,500+ | Dedicated Ryzen / current-gen with high clock | $50+ |
Absolute numbers drift with sysbench version, so compare candidates using the same binary and the same prime count. A 40% gap between two “same price” plans on this test is a 40% gap in your application’s response time at low concurrency — a difference no amount of nginx tuning can recover.
Axis 3: Disk Is Not “Size” — It Is IOPS Tier and Backing Store
Ask three questions in this order. First, is the storage local to the hypervisor or is it a network block device? Local NVMe gives sub-0.2 ms p99 at queue depth 1; network-attached storage is typically 0.5–2 ms and far more variable. Second, is there a published IOPS cap and does it scale with disk size? Third — and most important — what does the p99 look like after ten minutes of sustained writes, when any burst credit has been spent?
sudo fio --name=rw --ioengine=libaio --direct=1 --bs=4k --iodepth=32 \
--rw=randwrite --size=2G --runtime=600 --time_based \
--write_iops_log=/tmp/fio --log_avg_msec=10000 --group_reporting
# Read the per-10s IOPS from the log to see burst credit expire
awk -F, '{print $2, $8}' /tmp/fio_iops.1.log | sort -k2 -n | head -5
A disk that reports 20,000 IOPS in the first ten seconds and 1,200 IOPS from minute three onward is sold on burst credit. That is not a defect — it is a product — but it means your database write path will behave completely differently during a bulk import than during normal traffic. Test with the workload you will actually run, and test it for at least ten minutes.
Axis 4: Does the Guest Span One NUMA Node?
Within a guest you rarely see the physical topology, but you can infer it. If lscpu reports two sockets or two NUMA nodes on a 2-vCPU instance, the guest has been split across a boundary. Memory-bound workloads then pay remote-node latency on every access.
lscpu | grep -E 'Socket|Core|Thread|NUMA node'
numactl --hardware 2>/dev/null | head -8
# Compare local vs remote memory bandwidth
numactl --cpunodebind=0 --membind=0 sysbench memory --memory-block-size=1M \
--memory-total-size=4G --threads=1 run | grep 'transferred'
numactl --cpunodebind=0 --membind=1 sysbench memory --memory-block-size=1M \
--memory-total-size=4G --threads=1 run | grep 'transferred'
A 15% or larger drop on the remote-node run means memory bandwidth is node-local and your workload is placed badly. You cannot fix this from inside the guest; it is a placement decision made by the provider. It is also a legitimate reason to ask for the instance to be re-scheduled, if the provider supports it.
Axis 5: Network Path, Not Just Port Speed
Two providers in the same city can differ by 30 ms of RTT to your users if one has a direct peer with their ISP and the other backhauls traffic to a transit exchange in another country. Measure from the VPS to the actual eyeball networks your users are on, not to a generic “speed test” node.
for t in 1.1.1.1 8.8.8.8 9.9.9.9; do
echo "== $t"; mtr -rwzc 100 -i 0.2 $t | tail -1
done
# Number of distinct ASes on the path to your users
mtr -rwzc 50 -z | awk '{print $(NF-1)}' | uniq | wc -l
Turning Axes Into a Purchase Decision
Score each candidate on the axes that matter for the workload, not on a single composite number. A rule that works for most small production stacks:
- Static sites and small APIs — single-core speed first, latency to users second. vCPU count above two buys little.
- Transactional databases — disk p99 and NUMA placement first, RAM second, core count third.
- Build runners and batch jobs — sustained all-core CPU, and explicitly not burst-credit plans, because builds run long enough to spend the credit.
- Proxies and VPN endpoints — NIC throughput and per-flow vs aggregate shaping, with CPU only needing to keep up with the policer.
Run all five tests inside the first hour of a monthly-billed trial, keep the numbers, and re-run them at the end of the first week. If the week-two numbers match the hour-one numbers, you have found a provider whose allocation is stable — which is worth more than a marginally better headline spec. Our VPS plan sizing guide by workload maps each of these axes to concrete plan requirements.


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