Two servers can both say “NVMe SSD” and deliver wildly different database performance. The difference is IOPS under concurrency, and it is invisible unless you measure it correctly. Most people run a single sequential read, get a big number, and conclude the disk is fast. This guide shows the fio settings that actually reveal a plan’s storage behaviour.
Why Single-Stream Tests Mislead
A sequential read measures bandwidth, not IOPS. Databases, mail servers, and container hosts care about random 4K operations at depth. A budget NVMe pool with a hard IOPS ceiling can still push 1 GB/s sequentially while collapsing to 300 IOPS at random, which is what makes a MySQL query take 400 ms instead of 4 ms. If you only ever ran dd, you never saw the ceiling.
The Four Tests Worth Running
- Random read, 4K, queue depth 32 — the single most representative number for database work.
- Random write, 4K, queue depth 32 — reveals write amplification and thin-provisioning penalties.
- Mixed 70/30 read/write, 4K — closer to a real web application workload.
- Sequential read, 1M — useful only as a bandwidth ceiling check.
The Commands
Install fio (apt install fio) and run each test against a real file, not a RAM-backed path. Use a 4 GB file so caching cannot flatter the result, and give each test 60 seconds. Run them in the order listed so a write test does not warm the page cache for the read test.
fio --name=randread --rw=randread --bs=4k --iodepth=32 --size=4G --runtime=60 --time_based --filename=/root/ioftest --group_reportingfio --name=randwrite --rw=randwrite --bs=4k --iodepth=32 --size=4G --runtime=60 --time_based --filename=/root/ioftest --group_reportingfio --name=mixed --rw=randrw --rwmixread=70 --bs=4k --iodepth=32 --size=4G --runtime=60 --time_based --filename=/root/ioftest --group_reportingfio --name=seqread --rw=read --bs=1M --iodepth=8 --size=4G --runtime=60 --time_based --filename=/root/ioftest --group_reporting
Reading the Output
| Metric | Where to find it | What good looks like |
|---|---|---|
| IOPS | iops= in the report | 4K random read >2,000 on budget plans; >10,000 on real NVMe |
| clat (completion latency) | clat percentiles | p99 under 5 ms is healthy; over 20 ms causes visible app lag |
| latency distribution | lat (msec) block | Most operations in the first two buckets |
| Bandwidth | bw= | Sequential matters only for large file transfer |
The number to watch is p99 completion latency, not the headline IOPS. A disk averaging 4,000 IOPS with a p99 of 60 ms will produce occasional 500 ms page loads. One averaging 1,500 IOPS with a p99 of 3 ms feels instant. Averages hide the tail that users actually notice.
Interpreting What You See
| Pattern | Likely cause | Action |
|---|---|---|
| IOPS flat regardless of queue depth | Provider IOPS cap per volume | Move data to a plan with higher cap, or reduce random IO |
| Random write far below read | Thin provisioning / shared write cache | Enable write-back caching at the app layer, batch writes |
| p99 latency spikes every ~30 s | Noisy neighbour or host-level backup job | Re-test at different times; escalate with evidence |
| Excellent now, poor after 3 months | Data moved to a slower host pool | Re-run quarterly; keep historical CSVs |
Reducing IO Pressure Without Changing Plans
- Increase MySQL
innodb_buffer_pool_sizeso more reads hit RAM instead of disk. - Batch small writes — a single 100-row insert beats 100 single-row inserts.
- Move logs and temp tables to a separate volume if your provider offers one.
- Turn on the OS write cache (
vm.dirty_ratiotuned, not left at default) for bursty workloads. - Enable
noatimeon the data mount — it removes a metadata write on every file read.
Store each run’s CSV in a version-controlled folder. Storage performance on shared infrastructure drifts, and having three months of history turns a vague complaint into a support ticket with evidence. Most providers respond very differently when you can show a before-and-after table.
If the numbers show a genuine storage ceiling, the answer is not always a bigger plan — sometimes it is separating workloads so only the database sits on fast storage. You can compare budget VPS plans on our comparison table and check per-plan storage specs, then pair the results with our guide to server storage cost per gigabyte to avoid paying for capacity you do not use.
It also pays to understand where your storage stops being the bottleneck. Our breakdown of where budget VPS plans break first covers how IOPS, RAM, and CPU interact, so you can tell whether a slow query is really a disk problem or a memory problem masquerading as one.
Re-run the full sequence after any migration or “hardware upgrade” email. It is the fastest way to confirm that a promised improvement actually reached your instance — and the fastest way to catch the opposite.
Benchmarking Across Time, Not Just Once
A single benchmark is a snapshot; a series of them is a diagnostic tool. Run the same four fio tests once a month and keep the CSVs in one folder. After three months you have a baseline, and any deviation becomes visible immediately. A sudden 40% drop in random-read IOPS with no change on your side usually means the provider moved your volume or added tenants to the host — both worth a support ticket, and both almost impossible to prove without history.
Time your tests to the same hour each run. Storage performance on shared infrastructure follows a daily cycle: evening hours in the datacenter’s primary timezone are usually the busiest, and a test run at 3 a.m. will flatter a plan that struggles at 8 p.m. If your application serves users during peak hours, benchmark during peak hours. A number that only holds at 3 a.m. is not the number your users experience.
Finally, document the CPU model alongside each storage result. Providers occasionally migrate instances to newer hosts, and a storage regression can be offset by a CPU gain — or vice versa. Without recording both, you cannot tell which change you are actually observing.



