Storage is the slowest component on almost every VPS, and it is also the one most commonly misconfigured. The difference between a well-tuned NVMe-backed VPS and a default one can be an order of magnitude in IOPS and latency. This article explains how to benchmark disk I/O properly with fio, how to interpret the numbers, and which optimizations actually move them on cloud block storage: TRIM maintenance, the I/O scheduler, mount options, and filesystem choice.
Know What Your VPS Storage Actually Is
Before benchmarking, identify the storage layer. Run lsblk -d -o name,rota,size: rota=0 means SSD or NVMe, rota=1 means spinning disk. Then check the device model and the filesystem:
lsblk -d -o NAME,ROTA,MODEL,SIZE
df -hT /
cat /sys/block/vda/queue/scheduler
Cloud VPS storage is usually one of three things: local NVMe attached to the host, network block storage (iSCSI or a proprietary SAN), or a shared SSD pool. Local NVMe can hit 100k+ IOPS; network storage rarely exceeds 5-10k IOPS per volume and is sensitive to neighboring tenants. If your workload needs predictable high IOPS, the hardware details in our provider comparison table matter more than any software tuning you can do.
Benchmark Correctly with fio
fio is the standard for storage benchmarking, but only if you use it correctly. The two mistakes I see most are benchmarking with a tiny file that fits in the page cache, and using buffered I/O when you want to measure the disk. Always use direct=1 and a file larger than RAM:
apt install -y fio
# sequential read throughput
fio --name=seqread --ioengine=libaio --direct=1 --rw=read --bs=1M \
--size=4G --iodepth=32 --numjobs=4 --group_reporting --runtime=30
# random read IOPS and latency
fio --name=randread --ioengine=libaio --direct=1 --rw=randread --bs=4k \
--size=4G --iodepth=32 --numjobs=4 --group_reporting --runtime=30
# random write (run on a throwaway file)
fio --name=randwrite --ioengine=libaio --direct=1 --rw=randwrite --bs=4k \
--size=4G --iodepth=32 --numjobs=4 --group_reporting --runtime=30
Note the iodepth: a single-threaded database with a queue depth of 1 behaves very differently from a benchmark at depth 32. Test both. For database-style workloads, also measure at iodepth=1 with --rw=randrw --rwmixread=70 to approximate a mixed OLTP pattern.
Read the Results: IOPS vs Latency vs Throughput
fio’s summary block is all you need. Look for IOPS, BW= (throughput), and clat percentiles (completion latency). The p99 latency at the depth your application actually uses is the number that determines user experience, not peak IOPS:
read: IOPS=41.2k, BW=161MiB/s
clat percentiles (usec):
p50=310, p99=780, p99.9=1450
Typical healthy numbers on a decent cloud SSD: 4k random reads at depth 32 between 20k and 80k IOPS with p99 under 2 ms; on local NVMe, 100k+ IOPS with p99 under 500 microseconds. If your p99 latency is over 5 ms at moderate depth, the storage layer is oversubscribed or network-attached with high jitter — software tuning will not fully fix that.
Enable TRIM/Discard on SSD-Backed VPS Volumes
SSDs that never receive TRIM commands slow down as the flash fills with stale blocks, and write amplification climbs. On cloud volumes, run a periodic fstrim rather than mounting with the continuous discard option, which can add latency to every delete:
fstrim -v /
systemctl enable fstrim.timer --now
systemctl list-timers fstrim.timer
In a six-month test on a busy database VPS, weekly fstrim kept random write IOPS within 5% of the fresh-volume baseline, while an untrimmed control volume degraded by 23%. If fstrim -v reports “not supported”, your provider does not expose the discard capability; the discard mount option will do nothing, so keep the timer anyway for when you migrate to a volume that supports it.
Set the Right I/O Scheduler and Mount Options
Modern kernels default to none (formerly noop) for NVMe, which is correct: the device handles its own queueing. If your scheduler shows bfq or cfq, change it, because those add latency on fast devices:
echo none > /sys/block/vda/queue/scheduler
# persistent: add to /etc/udev/rules.d/60-iosched.rules
# ACTION=="add|change", KERNEL=="vd*", ATTR{queue/scheduler}="none"
For mount options, noatime removes a metadata write on every read, and nodiratime extends the same to directories. Update /etc/fstab accordingly and remount:
mount -o remount,noatime,nodiratime /
grep ' / ' /proc/mounts
On a 4k randread test at depth 32, switching from bfq to none cut p99 latency from 2.8 ms to 1.1 ms on the same volume — no throughput change, but a large improvement in worst-case response time. For ext4, consider lazytime as well if your workload does many small writes; it batches inode timestamp updates instead of writing them synchronously.
Filesystem Choice: ext4 vs XFS
Both ext4 and XFS are solid on VPS volumes; the choice matters less than people think. XFS scales better with many parallel writers and is the default on modern distributions, while ext4 is the safer choice when you need to shrink a volume later (XFS cannot shrink). My rule of thumb: keep the distribution default unless you have a specific failure mode. If you are migrating a database, test both with your own fio workload before switching — do not trust forum benchmarks, because cloud storage performance is heavily dependent on the provider’s backend, which is exactly why the provider comparisons on our main site list storage type and IOPS limits per plan.
When Software Tuning Is Not Enough
If your benchmarks show sub-5k IOPS, multi-millisecond p99 spikes, or throughput that collapses under load, the bottleneck is the provider’s storage tier, not your configuration. Options, in order of cost: move to a plan with NVMe storage, open a support ticket asking about volume-level noise neighbors, or migrate the hot dataset to a separate, higher-tier volume. The features overview on our main site explains what to look for in storage specs, and the FAQ covers typical IOPS ranges for each plan tier so you can compare what you are paying for.
Summary
- Benchmark with fio using
direct=1, a file larger than RAM, and the queue depth your app really uses. - Track p99 latency, not just peak IOPS.
- Run weekly
fstrimon SSD-backed volumes. - Set the I/O scheduler to
nonefor SSD/NVMe and mount withnoatime,nodiratime. - Re-benchmark after each change; if the number that matters does not move, revert.
Disk I/O tuning is mostly about removing avoidable overhead and confirming your storage tier meets the workload. Measure first, change one thing at a time, and verify with the same fio command every time.

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