Almost every “my VPS feels slow” ticket ends with a dd test that says 1.2 GB/s and proves nothing. This is a job-file-first approach to disk benchmarking that produces numbers you can actually act on: random IOPS at realistic queue depth, tail latency, and a repeatable way to catch storage throttling in the act.
Why dd numbers are misleading
# the classic (and useless) test
dd if=/dev/zero of=/var/tmp/test bs=1M count=2048 conv=fdatasync
# 2.0 GB copied (2147483648 bytes), 1.5 s, 1.3 GB/s
That 1.3 GB/s is the host page cache plus log buffer merging, not the device. dd cannot issue parallel requests, cannot report latency percentiles, and cannot express a mixed read/write pattern. It is still useful for exactly one thing: finding out whether a full disk will accept writes at all.
Install fio and run a 60-second 4K random test
sudo apt update && sudo apt -y install fio
mkdir -p /var/tmp/fiotest
cat > /var/tmp/fiotest/rand4k.fio <<'EOF'
[global]
directory=/var/tmp/fiotest
size=2G
runtime=60
time_based=1
group_reporting=1
lat_percentiles=1
percentile_list=50:95:99:99.9
norandommap=1
randrepeat=0
[randwrite-qd1]
rw=randwrite
bs=4k
iodepth=1
[randwrite-qd32]
rw=randwrite
bs=4k
iodepth=32
EOF
sudo fio /var/tmp/fiotest/rand4k.fio
Reading the output
| Metric | Example (good NVMe tier) | Example (throttled shared storage) |
|---|---|---|
| randwrite 4k qd1 IOPS | 18,000 | 420 |
| randwrite 4k qd32 IOPS | 96,000 | 1,900 |
| clat p50 / p99 | 0.05 ms / 0.31 ms | 2.3 ms / 41 ms |
| Sequential 1M read | 3.1 GB/s | 210 MB/s |
| IOPS at 90 s vs 5 s | -4% | -88% |
Two rules of thumb: a healthy NVMe-backed plan keeps qd1 4K writes above 5,000 IOPS and p99 latency under 1 ms. If qd32 barely improves on qd1 (under 2x), the provider is capping queue depth or IOPS – parallelism is being queued artificially.
Sequential and mixed workloads
cat > /var/tmp/fiotest/seq.fio <<'EOF'
[global]
directory=/var/tmp/fiotest
size=4G
runtime=45
time_based=1
group_reporting=1
[seqread]
rw=read
bs=1M
iodepth=8
[mixed-70-30]
rw=randrw
rwmixread=70
bs=16k
iodepth=16
EOF
sudo fio /var/tmp/fiotest/seq.fio --output-format=normal,terse
The mixed 70/30 test is the closest single proxy for a database plus web server sharing one volume. If mixed IOPS is less than 40% of the pure-write figure, expect query latency to spike whenever a backup or image resize runs.
Catching throttling in the act
Run the same job continuously and print a line per 10-second window. A flat curve means real capacity; a step down that never recovers means a token-bucket cap. The watchdog below logs the cliff with a timestamp.
for i in $(seq 1 30); do
fio --name=watch --directory=/var/tmp/fiotest --rw=randwrite --bs=4k \
--iodepth=16 --size=1G --runtime=10 --time_based --group_reporting \
--output-format=json 2>/dev/null | \
python3 -c "import json,sys;d=json.load(sys.stdin)['jobs'][0];print(f\"{__import__('time').strftime('%H:%M:%S')} iops={d['write']['iops']:.0f} p99={d['write']['clat_ns']['percentile']['99.000000']/1e6:.2f}ms\")"
done | tee /var/tmp/fiotest/ramp.log
Confirm a provider cap rather than a slow device by checking the accounting counters – container-based VPSes expose the throttle events directly:
cat /sys/fs/cgroup/io.stat 2>/dev/null
cat /sys/fs/cgroup/blkio/*.throttle.io_service_bytes 2>/dev/null
iostat -x 5 3 | egrep 'Device|nvme|vda|sda'
Pick the test that matches your workload
| Workload | fio parameters | Metric to watch |
|---|---|---|
| MySQL / PostgreSQL OLTP | randrw, bs=16k, rwmixread=70, iodepth=32 | p99 clat |
| Redis AOF or WAL-heavy writes | write, bs=4k, iodepth=1, fsync=1 | IOPS at qd1 |
| Backup and log shipping | write, bs=1M, iodepth=8 | sustained MB/s |
| Static file serving | randread, bs=128k, iodepth=16 | MB/s and CPU cost |
| Container image pulls | randread, bs=1M, numjobs=4 | aggregate MB/s |
Add --fsync=1 for database-style writes and the numbers will fall sharply on consumer-grade storage. That gap between buffered and fsync throughput is frequently larger than the difference between two hosting tiers, which is exactly why a single buffered test can lead you to the wrong conclusion.
Turn the numbers into a decision
- Keep the JSON output of every run. A month-old baseline is the only way to prove a node got busier.
- Benchmark at the traffic hour, not at 3 a.m., and note the time in the filename.
- Test the volume you actually use – separate data volumes can be faster or slower than the root disk.
- Re-run after any plan change; provider migrations reset your assumptions.
Keep a baseline file, not a screenshot
A benchmark is only useful if it can be compared later. Write results to a dated file with the environment attached, so a support ticket or a plan upgrade can be judged against evidence rather than memory.
mkdir -p ~/bench
OUT=~/bench/$(date -u +%Y%m%dT%H%MZ)-$(hostname).txt
{
echo "== environment"; lscpu | egrep 'Model name|CPU\(s\)'; uname -r; df -h /
echo "== randread 4k qd32"
fio --name=r --directory=/var/tmp/fiotest --rw=randread --bs=4k --iodepth=32 \
--size=2G --runtime=30 --time_based --group_reporting --output-format=json
} >> "$OUT" 2>&1
grep -o '"iops" : [0-9.]*' "$OUT" | head -3
Run it quarterly, and again the day after any performance ticket. The difference between the same iops measurement at 03:00 and at 09:00 tells you more about a provider than a year of uptime graphs.
If your own numbers show a cliff under load, no guest-side tuning will recover it – the storage class is the limit. Comparing guarantees, dedicated vCPU, and dedicated volumes is the next step; the VPS plans with full root access page lays out the tiers this test was written against.




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