A single fio run tells you one number and hides four. To characterise a VPS disk you need a small matrix of tests — random and sequential, read and write, blocked and unblocked — because providers throttle on different axes and the throttled axis is the one that will hurt you in production. This plan takes about twenty minutes on an idle box and produces numbers you can compare across plans or across months.
Rules before you start
- Run on an empty test file in a directory that is not the one serving your site. Writing 4 GB will evict the page cache and momentarily degrade the live application.
- Never write directly to a raw device on a VPS — you do not own the block device, and the provider’s snapshot layer may not expect it.
- Record the file size. A test whose working set fits in page cache measures RAM, not disk. Use
--sizeat least 2× the memory you expect the provider to cache, and--direct=1to bypass cache entirely. - Fix the runtime with
--time_based --runtime=60so every test is comparable and no single test runs long enough to trigger sustained-IOPS penalties before you intend it to.
The five tests that matter
| # | Profile | Represents | Key metric |
|---|---|---|---|
| 1 | 4k randread, QD32 | Database index lookups | IOPS, p99 latency |
| 2 | 4k randwrite, QD32 | InnoDB WAL, log writes | IOPS, p99 latency |
| 3 | 128k seqread, QD1 | Cold file reads, backups | MiB/s |
| 4 | 1M seqwrite, QD8 | Large dumps and imports | MiB/s |
| 5 | 4k randread, QD1 | Latency floor | clat p99 alone |
Test 5 is the one people skip and it is the most revealing. At queue depth 1 you are measuring the device’s intrinsic latency with no parallelism to hide it, which is what a single-threaded PHP request actually experiences. If test 1 reports 40,000 IOPS but test 5 shows a 9 ms p99, your app will feel slow under any concurrency. Different instance types have very different memory ceilings as well as disk ceilings, which is why both numbers belong in the same buying decision — see the full specs and pricing to make that comparison concrete.
Running the suite
apt install -y fio
mkdir -p /root/fiotest && cd /root/fiotest
# Prepare a 4 GiB file once, then reuse it for read tests
fio --name=prep --filename=fiotest.dat --size=4G --rw=write \
--bs=1M --direct=1 --ioengine=libaio --group_reporting
# Test 1: random read IOPS
fio --name=randread --filename=fiotest.dat --rw=randread --bs=4k \
--iodepth=32 --direct=1 --ioengine=libaio --time_based \
--runtime=60 --group_reporting
# Test 5: latency floor, no queueing
fio --name=latency --filename=fiotest.dat --rw=randread --bs=4k \
--iodepth=1 --direct=1 --ioengine=libaio --time_based \
--runtime=60 --group_reporting
Reading the output correctly
Ignore the summary’s iops line until you have read latency. The useful block looks like this:
read: IOPS=12.4k, BW=48.4MiB/s (50.8MB/s)
lat (usec): min=180, max=41201, avg=2570.44
clat percentiles (usec):
| 1.00th=[ 404], 5.00th=[ 519], 10.00th=[ 619],
| 50.00th=[ 1188], 90.00th=[ 5408], 95.00th=[ 6745],
| 99.00th=[10816], 99.50th=[12544], 99.90th=[18048]
Read two things. First, the spread between p50 and p99: a 1.2 ms median with a 10.8 ms p99 is a device with unpredictable tail latency, typically a shared network-backed volume where a neighbouring tenant is bursting. Second, compare p99 across runs — if test 1 and test 5 have nearly identical p99 values, adding queue depth bought you nothing and the device is already saturated.
Hunting for throttle ceilings
Most VPS providers cap sustained IOPS. The cap is invisible until you cross it. To find it, run a longer burst and watch for a step change rather than a curve:
# 5 minutes of sustained random write, sampled every second
fio --name=burst --filename=fiotest.dat --rw=randwrite --bs=4k \
--iodepth=32 --direct=1 --ioengine=libaio --time_based \
--runtime=300 --group_reporting --eta=always --eta-interval=1
# Watch the kernel's own view from a second shell
iostat -x 1 | awk 'NR==3 || $1 ~ /^(sd|nvme|vd)/'
cat /proc/pressure/io # PSI: some avg10 rising means real queueing
A smooth IOPS decline over five minutes is normal page-cache and journal behaviour. A sudden drop to a fixed plateau — say 12,000 IOPS down to 2,500 and staying there — is a provisioning policy. That plateau is the number to plan against, because a nightly backup plus a database dump can hit it simultaneously and neither will report an error; they will both simply take four times longer.
Turning numbers into a decision
- Record every run with its timestamp, file size, and hostname. Disk performance on shared storage changes as the host fills up.
- If p99 latency exceeds roughly 20 ms at queue depth 1, that instance cannot serve a latency-sensitive database comfortably.
- If sustained IOPS is under half the burst figure, size your backup window for the sustained number.
- Re-run the suite after any provider-side migration or plan change. Assumptions have a shelf life.
Adopting a fixed run cadence matters more than any single test. Run the suite on the day you provision an instance, then again after the host has been live for a month — shared storage performance tends to drift as the physical host fills up. Keeping the JSON output of each run (--output-format=json) in version control turns a pile of anecdote into a trend line you can point at when a provider claims nothing changed.
When the numbers say the disk is the wall, no amount of application tuning will move it — and that is useful information rather than a dead end. Layer these results against the CPU and memory ceilings you have already measured: Our VPS comparison page puts storage tiers beside those figures instead of leaving you to guess from marketing IOPS claims.

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