Understanding VPS: Virtual Private Server Definition, at the Hypervisor Level

Every plan page says “2 vCPU”. Almost none of them mean the same thing. On one provider that is two hyperthreads of a single physical core with a 1:8 overcommit ratio; on another it is two pinned cores on a 3.4 GHz EPYC with a guaranteed run queue. Both are legal descriptions of “2 vCPU”, and the workloads that separate them are not exotic — a Go build, an ffmpeg transcode, or Postgres checkpointing will land 2.4× apart on the two.

The definition worth using is mechanical rather than commercial: a VPS is a guest whose CPU time, memory pages, I/O requests, and syscall surface are each metered and limited by the hypervisor. If you can name which kernel subsystem enforces each limit, you can predict how the instance behaves under load. Shared and pinned plans differ mainly in which of those limits apply, and that difference is measurable rather than a matter of opinion.

Four mechanisms do the work: CPU quota and weight, the balloon driver, paravirtual I/O queues, and the host cgroup hierarchy. Each leaves a fingerprint inside the guest.

CPU: quota, weight, and the missing vCPU

Two controls determine how much CPU you actually get. cpu.max (cgroup v2) or cpu.cfs_quota_us (v1) is a hard ceiling: 200000/100000 means two full cores’ worth of time per 100 ms window. cpu.weight is relative: with three tenants at weights 100/100/100, an idle host gives you everything; a saturated host gives you one third. Marketing “vCPU” numbers describe the first value and quietly omit the second.

Inside the guest, nproc reports the ceiling, not the entitlement. The gap shows as steal time: four runnable threads against a two-core quota lose half the wall time before the guest scheduler sees it.

# what the guest believes it has
nproc; grep -c ^processor /proc/cpuinfo
# what it is actually allowed to consume (host side names differ)
cat /sys/fs/cgroup/cpu.max 2>/dev/null
# pressure: 'some' rising with 'full' near 0 = queued, not running
cat /sys/fs/cgroup/cpu.pressure
# the fingerprint: non-zero steal with idle time present
mpstat -P ALL 1 5

A second-order effect matters as much: whether the two vCPUs are siblings on one physical core. If they are, a memory-bound workload that scales to 1.8× on two real cores instead reaches about 1.25×. host-passthrough exposes core id and thread siblings so you can see it.

Memory: ballooning is not swap

The guest sees a fixed RAM size, but the host can reclaim pages through virtio_balloon. When the host is under pressure it asks the guest driver to inflate the balloon, and the guest kernel hands back pages it considers free — clean page cache first, then reclaimable slab. From inside, this looks like your working set shrinking: MemAvailable drops while nothing you run has changed.

The failure mode is subtle because it is not an error. A JVM with a 6 GB heap on an 8 GB guest whose balloon took 2 GB re-reads its own class data from disk, and the only symptom is GC timing drifting upward over hours.

I/O: virtio queues and their depth

A virtio-blk device exposes a fixed number of queues, each with a fixed ring depth; /sys/block/vda/queue/nr_requests is your side of that contract. With num-queues=1 and depth 128, every request from your guest and every other guest on that device contends for the same submission path. A dedicated device or a passed-through namespace removes the cross-guest contention entirely, which is why IOPS medians look similar and p99.9 tails differ by 10×.

Virtio-net is the same at packet level. If the host allocates fewer TX queues than the guest expects, packets queue in the guest’s tc layer and netstat -s shows retransmits unrelated to the network. Compare ethtool -l eth0 combined channels against your vCPU count.

I/O limits: the throttles you never see

Host cgroup io.max caps bytes and IOPS per device. When you hit it, requests are delayed before reaching the device, and the guest kernel attributes that delay to the device: await climbs with %util near 100% on an unsaturated device. If a second process slows proportionally, you are throttled, not queued.

Symptom in guestLikely enforcerConfirms via
Smooth throughput cap, flat p99io.max byte throttlecap scales with block size
p99.9 spikes, median fineshared virtio queueaqu-sz > own depth
%steal 3–10%, idle >0cpu.weight contentionmatches neighbour bursts
MemAvailable falls, no workload changeballoon inflationDeflate in /proc/vmstat
Slow tcp, zero retransmitsnet TX queuesethtool -l mismatch

Why the same “2 vCPU” label means three different machines

Three plans, all labelled 2 vCPU / 4 GB. A: two hyperthreads, weight 100, shared depth-128 queue. B: two pinned cores, dedicated queue, reserved RAM. C: a 4-vCPU quota at weight 50 on a packed host. A single-threaded benchmark puts A and B within 8%, because one thread never touches the contention path. An nginx TLS proxy at 3k req/s puts them 3.1× apart.

This is why benchmarks that measure one thread are useless for choosing a plan, and why “2 vCPU” plus a high burst number proves nothing. The label describes capacity; the enforcer determines delivered latency. The only honest way to compare is to saturate the resource you care about and look at the tail.

What to measure before you trust a label

Run a 60-second saturation test per resource, not an all-at-once stress test. Record %steal, MemAvailable delta, and p99.9, then re-run all three concurrently. Concurrent p99.9 above 2× the isolated figure means a shared enforcer sits somewhere in the path.

None of this requires access to the hypervisor. It requires knowing which kernel structures enforce each limit and what they look like from below. That is the whole hypervisor-level definition of a VPS: not “a server in the cloud”, but a set of cgroup files, a balloon driver, a bounded virtio ring, and a scheduler weight. Compare plans on those four numbers rather than the marketing column, and a spec sheet that names pinned cores and exclusive queues becomes a claim you can test in an afternoon instead of a slogan you have to believe.

Leave a Reply