Diagnosing Disk I/O Latency Spikes on a VPS Before They Wreck TTFB

A VPS with plenty of free RAM and idle CPU can still feel slow. When pages take 1.5 seconds to render and top shows no obvious culprit, the problem is usually storage latency — the time a single I/O request waits before the kernel hands it to the block layer. This article gives you a repeatable diagnostic sequence: measure latency (not throughput), attribute it to a process, and fix the layer that is actually responsible. If you are still choosing a platform for this kind of workload, our cloud VPS benefits breakdown explains why burst-then-throttle storage quotas matter here.

Why latency, not throughput, is the metric that matters

Disk throughput (MB/s) tells you how fast data moves in bulk. Latency tells you how long one request waits. A database-backed web page issues dozens of small, random reads. If each random read waits 20 ms in a queue, 40 reads become 800 ms of wall-clock time regardless of how many gigabytes per second the underlying array can stream.

Rule of thumb on NVMe-backed VPS instances:

  • Average read latency under 1 ms: healthy for a shared-tier VPS.
  • 1–5 ms: elevated but workable; investigate application query patterns.
  • Above 10 ms sustained: you are being throttled, oversubscribed, or doing something pathological.

Step 1: confirm you have an I/O problem at all

Start with iostat, which separates CPU and device statistics. Install it with apt install sysstat on Debian/Ubuntu, then run a 1-second sample for 30 iterations:

iostat -xz 1 30

Ignore the first line (it reports lifetime averages since boot). Read the %util and await columns on the device that backs your filesystem — usually vda, sda, or nvme0n1. Two distinct failure modes look similar but need different fixes:

Pattern%utilawaitLikely cause
Queue saturation> 90%High, risingsToo many concurrent requests; application or filesystem issue
ThrottlingLow (< 30%)HighProvider storage quota; requests deliberately delayed
Cache-miss stormsModerateVariable, spikyWorking set larger than available page cache

The throttling row is the one people miss. A low %util combined with high await means the device is not busy — the hypervisor is deliberately metering you. No amount of local tuning will fix that; you need a plan with a higher I/O allotment. Benchmarking the ceiling first is the honest move, as covered in our fio and dd benchmarking walkthrough.

Step 2: attribute latency to a process

Knowing the disk is slow is not the same as knowing who made it slow. iotop shows per-process I/O in real time:

apt install iotop
iotop -oPa -d 2

The -o flag shows only processes doing I/O, -P shows processes rather than threads, and -a shows accumulated rather than instantaneous bandwidth. Let it run during an actual latency spike. If you cannot be at the console when it happens, use pidstat to log historically:

pidstat -d 5 720 > /var/log/pidstat-io.log 2>&1 &

That records one hour of per-process read/write KiB/s. When the spike happens, correlate timestamps. In practice the top three offenders on a small VPS are:

  1. MySQL/MariaDB flushing dirty pages. The InnoDB log and doublewrite buffer produce bursty writes. If Innodb_data_pending_fsyncs climbs, your innodb_io_capacity is set too high for the disk you actually have.
  2. Backups and snapshot agents. A nightly tar or restic job reading the whole filesystem evicts your database’s hot pages from page cache. The latency spike at 03:00 is really a cache-cold problem at 03:05.
  3. Log rotation and journald syncs. Small but constant: check iotop during idle periods.

Step 3: prove whether it is queue depth or device speed

Use iostat -x with attention to the aqu-sz (average queue size) and r_await versus w_await columns. Separate read and write latency is the single most useful split, because the fixes diverge completely:

iostat -x 1 10 | awk 'NR==1 || $1 ~ /^(vda|sda|nvme0n1)$/'

High w_await with normal r_await means write amplification or fsync stalls. Check innodb_flush_log_at_trx_commit (set to 2 trades one second of durability for a large reduction in fsync pressure) and confirm your filesystem is not doing copy-on-write overhead you did not intend. High r_await with normal w_await means your working set no longer fits in cache — the answer is usually memory, not disk.

Step 4: the cheap wins that resolve most spikes

Before upgrading a plan, apply these in order and re-measure after each. Real-world impact percentages below come from typical small-VPS LAMP/LEMP stacks (1–2 vCPU, 2–4 GB RAM) in production.

  • Cap InnoDB flush rate to the disk you have. Set innodb_io_capacity = 200 and innodb_io_capacity_max = 400 on shared-tier storage. This alone removed 60–80% of write latency spikes in the worst offending benchmarks, at the cost of slightly slower checkpoint completion.
  • Move the database log to a path with less churn, or enable O_DIRECT. innodb_flush_method = O_DIRECT avoids double-buffering through page cache — often a 20–30% latency reduction on write-heavy workloads.
  • Serialize your backups. Wrap restic or tar in ionice -c2 -n7 nice -n19. This drops backup-induced page-cache eviction dramatically because idle-class I/O yields to interactive reads.
  • Check scheduler. none (or mq-deadline) is correct for NVMe; bfq is rarely the right answer on virtual block devices, since the hypervisor already schedules.
# Apply I/O class to the backup job without editing the script
ionice -c2 -n7 nice -n19 /usr/local/bin/backup.sh

# Check the current scheduler for the root device
cat /sys/block/vda/queue/scheduler

Step 5: when the numbers say “stop tuning, resize”

Tuning has a hard ceiling when the constraint is a provider quota. Three signals mean you are there: await stays above 10 ms after all of the above, %util remains low while latency is high, and a fresh fio run on an idle system cannot beat 300 IOPS at 4K random read. At that point the workload has outgrown the tier, not the configuration.

Two upgrade paths worth pricing before you over-provision blindly: a plan with dedicated NVMe and a documented IOPS floor, or simply running the database on its own instance so its I/O never competes with web serving. Comparing the second option against a single beefier box is where the VPS versus dedicated server trade-off becomes concrete — dedicated hardware usually wins on sustained random I/O and loses badly on price per unit of steady-state load.

For shared-tier instances where you want predictable I/O without a dedicated box, InterServer’s storage-backed plans are worth benchmarking against your current provider: InterServer VPS plans. Run the same fio command you used on the old host and compare clat percentiles, not just the average.

A 10-minute checklist you can run right now

# 1. Sustained device picture
iostat -xz 1 30

# 2. Who is doing the I/O
iotop -oPa -d 2

# 3. Is it read or write?
iostat -x 1 10 | awk 'NR==1 || $1 ~ /^(vda|sda|nvme0n1)$/'

# 4. Is it page cache pressure?
grep -E 'dirty|writeback' /proc/meminfo
cat /proc/pressure/io

# 5. Is it the provider throttling?
#    low %util + high await = yes

/proc/pressure/io is the file most people do not know exists. It reports some and full stall percentages over 10/60/300-second windows — a direct measure of tasks blocked on I/O, which is a far better early-warning signal than queue depth. If full stays above 5%, real work is stalling.

What to fix first, in one sentence per case

  • Low %util, high await → provider throttle → resize or change provider.
  • High %util, high w_await → write pressure → cut innodb_io_capacity, use O_DIRECT, ionice your backups.
  • High %util, high r_await → cache misses → add RAM or shrink the working set.
  • Spiky, low average → bursty neighbor or backup → move jobs to off-peak, use idle I/O class.

Work the sequence top to bottom and you will fix the right layer instead of guessing at kernel parameters. And if the diagnosis keeps pointing at resources rather than configuration, the honest conclusion is that the current plan is the bottleneck — not your tuning. Where that happens often on storage-heavy stacks, checking managed options for the database tier is a reasonable next step; Cloudways’ managed hosting with pre-provisioned NVMe storage removes the platform-tuning half of this problem entirely.

Leave a Reply