If you cut your teeth on bare-metal servers, you remember the elevator tuning ritual: deadline for databases, noop for SSDs, BFQ for desktops. On a cloud VPS, that instinct mostly wastes your time — because the guest never sees a real disk. The I/O scheduler in your VM’s kernel reorders requests that are about to be shoved through a virtual queue into a hypervisor that does the real scheduling. This article explains what the scheduler actually does on a virtio-blk device, when it still matters, and where your tuning effort belongs instead.
What your VPS actually presents to the kernel
lsblk -d -o NAME,ROTA,TRAN
# NAME ROTA TRAN
# vda 0 virtio
cat /sys/block/vda/queue/scheduler
# [mq-deadline] none
Three things jump out: the device is virtio (a paravirtualized block device), it is non-rotational (ROTA 0), and the default scheduler on modern kernels is mq-deadline with none as the alternative. There is no BFQ in sight unless your provider passes through a real NVMe controller — which is rare on budget and mid-range VPS plans.
Why the guest scheduler barely matters
On a physical box, the scheduler’s job is to merge adjacent requests and reorder them to minimize head movement — a big deal for spinning disks, a smaller deal for SSDs. Inside a VM, in-flight requests are limited by the virtio ring (typically 128 entries), and the hypervisor performs merging and scheduling on the host side, where it sees the real storage. Your guest scheduler can only reorder the requests sitting in its own small queue, so its impact on total throughput is a few percent at most. Anyone promising large gains from switching a cloud VPS to none is selling something.
The one place the guest queue can bite is at very high queue depth: bulk imports, analytics jobs, or a database checkpoint writing hundreds of MB at once. If iostat shows avgqu-sz pinned at the queue limit with utilization near 100%, the guest queue — not the storage — is the bottleneck. That is the scenario where none can shave a few percent of overhead.
The levers that do exist
nr_requests controls how many requests the block layer queues per device. The default of 128–256 is right for most workloads; lowering it to 64 can trim latency spikes when a noisy neighbor saturates the shared storage, and raising it almost never helps on virtio because the ring caps what can actually be in flight.
iodepth is the parameter that actually moves numbers — but it belongs in your benchmark and your database configuration, not in /sys. A MySQL or PostgreSQL instance issuing depth-16 to depth-32 I/O will behave completely differently from a depth-1 benchmark:
fio --name=randread --rw=randread --bs=4k --iodepth=32 \
--ioengine=libaio --direct=1 --runtime=60 --numjobs=4 --size=1G
Run that once per scheduler to prove the point to yourself:
for sched in none mq-deadline; do
echo "$sched" > /sys/block/vda/queue/scheduler
fio --name=bench --rw=randread --bs=4k --iodepth=32 \
--ioengine=libaio --direct=1 --runtime=30 --size=512M \
| grep -E 'IOPS|lat'
done
On a typical virtio disk the IOPS difference lands inside the noise. If you do decide to change the scheduler, make it survive reboots with a udev rule:
# /etc/udev/rules.d/60-iosched.rules
ACTION=="add|change", KERNEL=="vd*", ATTR{queue/scheduler}="none"
sudo udevadm trigger
How to tell if the guest queue is even the bottleneck
Before you touch any knob, confirm the queue is the constraint. iostat -x 5 gives you the numbers that matter: avgqu-sz (average queue length), %util (how busy the device reports itself), and await (average I/O time including queueing). On a virtio disk with a 128-entry ring, avgqu-sz pinned near 128 with %util at 100% and await climbing means requests are waiting in the guest — the point where scheduler and nr_requests choices can shave latency. If instead avgqu-sz stays low but await is high, the storage backend is slow or throttled, and no guest-side setting will fix it — that’s a provider conversation, not a kernel one.
The same separation applies to vmstat: watch the b column (processes blocked on I/O). A few blocked processes during a checkpoint is normal; sustained double digits across all vCPUs points at the storage layer, and it’s worth re-running your fio baseline against the provider’s published numbers before you blame your config.
Where to spend the tuning time instead
The scheduler is a five-minute check, not a tuning project. The real wins on cloud storage live in three places:
- The database’s own I/O knobs.
innodb_io_capacity/innodb_io_capacity_maxin MySQL,effective_io_concurrencyin PostgreSQL, checkpoint intervals, and fsync batching all change real-world throughput on virtual disks. - Filesystem mount options.
noatimeeliminates a write on every read, and a weeklyfstrim -avkeeps the SSD’s flash translation layer from degrading — without the overhead of thediscardmount option on every delete. - Knowing your baseline. If IOPS collapse regardless of scheduler, suspect the provider, not the kernel. Run the same fio job on two candidates and check CPU steal (
top‘s%st) while you are at it — storage specs vary more between providers than any scheduler setting ever will. We track disk type and performance in our VPS comparison table.
Verify the default scheduler is mq-deadline, run one fio at realistic depth to know your storage’s ceiling, then go tune the database. That is the order that produces measurable gains on a VPS.

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