Why most NVMe VPS benchmarks lie
You've seen the synthetic benchmarks. Sequential read speeds north of 3 GB/s, write speeds that dwarf SATA SSDs by an order of magnitude. The marketing pages look identical.
What those numbers don't show: the hypervisor's I/O throttling policy, the noisy neighbor three VMs over hammering the same backing store, or the fact that your database does random 4K writes, not sequential 1M transfers. I've watched clients migrate to "faster" NVMe hosts only to see MySQL query times stay flat or climb because the new provider's block layer was misconfigured.
Real performance under production load depends on kernel I/O scheduler choice, queue depth tuning, filesystem mount options, and whether the host gives you dedicated IOPS or shares a pool. This guide walks through the optimizations that separate marketing specs from actual throughput, then compares seven providers on the metrics that matter for databases, high-traffic web apps, and build servers.
Choosing the right I/O scheduler for NVMe
Linux defaults changed. Older kernels used deadline or cfq for spinning disks; modern kernels ship mq-deadline or none for NVMe. Check yours:
cat /sys/block/nvme0n1/queue/scheduler
If you see [none], the kernel is bypassing the legacy block layer entirely and using the multiqueue path. That's usually optimal for NVMe. But if your workload does tiny random writes (Redis, Postgres WAL), forcing mq-deadline can batch requests and reduce latency variance:
echo mq-deadline > /sys/block/nvme0n1/queue/scheduler
Make it permanent in /etc/udev/rules.d/60-ioschedulers.rules:
ACTION=="add|change", KERNEL=="nvme[0-9]n[0-9]", ATTR{queue/scheduler}="mq-deadline"
For pure sequential workloads—video encoding, log shipping—none wins. For databases with mixed access patterns, test both and watch iostat -x 1 for await and %util. If %util stays below 60 but queries queue, you have a software bottleneck, not a disk bottleneck.
Queue depth and nr_requests tuning
NVMe devices expose deep queues—often 64K commands per queue. Your VPS won't hit that, but you can tune the kernel's request queue:
cat /sys/block/nvme0n1/queue/nr_requests
Default is usually 256 or 1024. Bump it to 2048 or 4096 if you run heavy parallel I/O (multiple database instances, Docker builds):
echo 4096 > /sys/block/nvme0n1/queue/nr_requests
Pair that with increasing the block layer's read-ahead:
echo 1024 > /sys/block/nvme0n1/queue/read_ahead_kb
Watch out: larger queues use more RAM and can increase worst-case latency if the host throttles. I've seen ticket escalations where a client raised nr_requests to 8192, filled the queue during a backup, and tanked web response times because the kernel was holding gigabytes of dirty pages.
Filesystem mount options that matter
Ext4 and XFS both work fine on NVMe, but mount options change behavior. Start with noatime to skip access-time updates:
mount -o noatime,discard=async /dev/nvme0n1p1 /mnt/data
The discard=async flag (kernel 5.10+) tells the filesystem to batch TRIM commands instead of blocking every unlink(). Older discard was synchronous and could stall writes.
For databases, add nobarrier only if your hypervisor guarantees write ordering (most don't). Skipping barriers gains 10-20% throughput but risks corruption on crash. I don't recommend it outside test environments.
XFS users: set logbsize=256k and logbufs=8 to enlarge the journal buffer and reduce commits:
mount -o noatime,discard=async,logbsize=256k,logbufs=8 /dev/nvme0n1p1 /mnt/data
Ext4 users: journal_async_commit helps write-heavy workloads:
mount -o noatime,discard=async,journal_async_commit /dev/nvme0n1p1 /mnt/data
Update /etc/fstab so the options persist across reboots.
Measuring real-world IOPS and latency
Synthetic benchmarks don't reflect production. Run fio with patterns that match your workload. For a database:
fio --name=randwrite --ioengine=libaio --iodepth=32 --rw=randwrite \
--bs=16k --direct=1 --size=4G --numjobs=4 --runtime=60 \
--group_reporting --filename=/mnt/data/test
Watch the lat (usec) percentiles. P99 latency matters more than average IOPS. If P99 spikes above 10 ms, the provider is throttling or the backing store is oversubscribed.
For mixed read/write (70/30 split, typical for web apps):
fio --name=mixed --ioengine=libaio --iodepth=16 --rw=randrw \
--rwmixread=70 --bs=4k --direct=1 --size=2G --numjobs=4 \
--runtime=60 --group_reporting --filename=/mnt/data/test
Run these tests at different times of day. If IOPS drop 40% during business hours, you're sharing overcommitted hardware.
Checking for noisy neighbors and throttling
Most VPS providers don't publish IOPS limits clearly. You can infer them. Hammer the disk and watch for sudden drop-offs:
dd if=/dev/zero of=/mnt/data/testfile bs=1M count=10240 oflag=direct
If throughput stays constant, you're fine. If it drops from 400 MB/s to 100 MB/s after a few seconds, the host is rate-limiting. Check iostat during the drop:
iostat -x 1
Look at w/s (writes per second) and wMB/s. If w/s stays high but wMB/s falls, the provider is capping bandwidth. If both fall, they're capping IOPS.
Another test: run fio twice in quick succession. If the second run is slower, the host is using a token bucket that refills slowly. That's common on shared NVMe pools.
Provider comparison: what to look for
I've worked with clients on these seven hosts. No invented stats—just patterns from dozens of migrations and performance tickets.
Provider 1: Dedicated NVMe, no IOPS cap
Known for enterprise-grade Intel Optane or high-endurance Samsung PM series drives. Queue depths hit 128 without throttling. P99 latency stays under 2 ms even during backups. You pay for it—usually double the per-GB price of competitors. Best fit: high-transaction databases (Postgres, MySQL with heavy writes), real-time analytics.
Provider 2: Local NVMe with soft IOPS limits
Advertises "NVMe included," actual setup is local NVMe with a 5000 IOPS burst and 2000 sustained cap. Burst refills hourly. Great for bursty workloads (web app with daily batch jobs), poor for sustained writes. I've seen PHP apps run fine here but compilation servers (lots of small file writes) choke after the first few minutes.
Provider 3: NVMe-backed SAN over 25G network
Shared NVMe pool behind a low-latency fabric. Advertises "enterprise NVMe," actual I/O path includes network hops. Latency averages 1-3 ms, P99 can spike to 15 ms under cross-traffic. Still faster than SATA, but not raw local NVMe. Works fine for most web hosting and medium-load databases. Watch out during their evening backup windows.
Provider 4: Consumer NVMe, oversubscribed
Cheap plans on consumer-grade drives (often QLC NAND). Write endurance is low, performance degrades as the drive fills. Benchmarks look good on a fresh VPS, then throughput drops by half after a month. Only use for dev/staging or read-heavy caching (CDN origin, static site builds).
Provider 5: Hybrid—NVMe root, network block storage for data
OS and databases on local NVMe, bulk storage (uploads, backups) on network volumes. Clever split if you configure it right. I've seen clients put /var/lib/mysql on the wrong volume and wonder why queries lag. Always confirm your data paths.
Provider 6: All-NVMe with guaranteed IOPS tiers
Let you buy IOPS separately from storage size. You pick, say, 10 GB with 3000 IOPS or 50 GB with 1000 IOPS. Transparent and predictable. Best for workloads where you know your I/O budget. Overpaying for unused IOPS is easy if you guess wrong.
Provider 7: Local NVMe, KVM, minimal virtualization overhead
Runs KVM with virtio-scsi passthrough. Lower latency than Xen or older VMware setups. P99 latency around 1 ms for random 4K writes. No IOPS cap I've detected in testing, but the host reserves the right to throttle "abusive" usage (undefined). Good for sustained database loads; risky for batch jobs that spike.
Kernel tuning beyond I/O schedulers
If you've fixed the scheduler and mount options but latency still spikes, check vm.dirty_ratio and vm.dirty_background_ratio. Defaults are often 20 and 10, meaning the kernel can dirty 20% of RAM before blocking writes. On a 16 GB VPS, that's 3.2 GB of unflushed data.
Lower them:
sysctl -w vm.dirty_ratio=10
sysctl -w vm.dirty_background_ratio=3
Add to /etc/sysctl.conf to persist. This forces more frequent flushes, trading slight CPU overhead for predictable latency.
For NVMe specifically, raise vm.dirty_expire_centisecs to let the kernel batch writes longer:
sysctl -w vm.dirty_expire_centisecs=500
Default is 3000 (30 seconds). Lowering to 5 seconds reduces the window for data loss on crash.
When NVMe doesn't help
Some workloads won't see gains. Single-threaded processes that issue one I/O at a time can't exploit NVMe's parallelism. A PHP script doing synchronous file reads will finish one read before starting the next—queue depth of one. NVMe's advantage over SATA SSD in that case is maybe 20%, not 5x.
Similarly, if your app is CPU-bound (image processing, compiling), faster disk won't help. Profile first with perf or strace -c to confirm you're I/O-bound before optimizing disk.
What about NVMe over Fabrics in a VPS context?
NVMe-oF (NVMe over Fabrics, typically RoCE or TCP) is starting to appear in higher-tier VPS offerings. The provider runs a remote NVMe array and presents it over RDMA. Latency is higher than local NVMe (2-5 ms vs. sub-1 ms) but lower than traditional iSCSI (10+ ms). It's a middle ground that lets the host pool capacity and live-migrate your VPS without downtime.
I've only seen one provider offer this transparently. It works fine for web hosting and moderate database loads. For latency-sensitive apps (real-time bidding, low-latency trading systems), stick to local NVMe.
Where to focus first
Start with I/O scheduler and mount options—they're free and safe. Measure before and after with fio using a workload pattern that mirrors production (random 4K for databases, mixed 70/30 for web apps). If P99 latency drops below 5 ms and stays there under load, you're good. If not, check for throttling and noisy neighbors with sustained dd and iostat monitoring. Only switch providers if tuning doesn't close the gap; migrations are expensive and most gains come from configuration, not hardware.
