Skip to main content
RunBook Academy

Proxmox VEIX · Virtual MachinesStorage in VMs

Disk I/O paths: aio, cache, iothread and how they interact

Expert⏱ ~26 min🧪 Lab requiredqmfio

What you'll learn

  • Describe what native, threads and io_uring do differently in the QEMU block layer
  • State which cache modes are compatible with which aio backend, and why
  • Explain what iothread changes and the controller requirement that gates it
  • Measure a configuration change instead of adopting it from a forum post

Prerequisites

Verified against Proxmox VE 9.2.4 · Proxmox Backup Server 4.2.5 · Ceph Squid / Tentacle · Debian 13 (Trixie) · Linux kernel 7.0 (PVE 9.2 default) · 2026-08-12

Not yet marked complete on this device.

Proxmox disk tuning advice circulates as isolated recommendations: use cache=writeback, enable iothread, set aio=native. Each of those is correct in some situation, none is correct universally, and two of them constrain each other in a way that is not signposted anywhere in the interface.

The previous lesson covered the controller choice, cache modes and IO threads. This one adds the parameter that is usually left at its default — aio — and then treats the four as one system, because that is what they are.

The four parameters

scsi0: <storage>:vm-118-disk-0,aio=<native|threads|io_uring>,
       cache=<directsync|none|unsafe|writeback|writethrough>,
       iothread=<1|0>,discard=<on|ignore>
ParameterDecides
aioHow the host submits I/O to the kernel
cacheWhether the host page cache is involved, and when the guest is told a write completed
iothreadWhich thread does the work
discardWhether guest TRIM reaches the storage

aio: three ways to submit I/O

ValueMechanismRequires O_DIRECTNotes
threadsA pool of worker threads issues ordinary blocking system callsNoWorks with every cache mode. Highest per-request overhead
nativeLinux kernel AIO (libaio)YesLow overhead, but only truly asynchronous on O_DIRECT file descriptors
io_uringThe modern io_uring submission and completion ringNoAsynchronous with or without O_DIRECT, fewer system calls per request

cache: what the guest is told, and when

The documentation frames this as a signalling question rather than a performance one:

Setting the Cache mode of the hard drive will impact how the host system will notify the guest systems of block write completions. The No cache default means that the guest system will be notified that a write is complete when each block reaches the physical storage write queue.

ModeHost page cacheWrite acknowledged whenSurvives a host crash
none (default)BypassedThe block reaches the physical storage write queueYes, if the storage honours flushes
directsyncBypassedThe block is on stable storageYes
writethroughUsed for readsThe block is on stable storageYes
writebackUsedThe block is in the host page cacheOnly data the guest flushed
unsafeUsed, and flush requests are ignoredImmediatelyNo

iothread: which thread does the work

The documented behaviour and its constraint:

The option IO Thread can only be used when using a disk with the VirtIO controller, or with the SCSI controller, when the emulated controller type is VirtIO SCSI single. With IO Thread enabled, QEMU creates one I/O thread per storage controller rather than handling all I/O in the main event loop or vCPU threads.

Two things follow, and the second is the one people miss.

The controller requirement is absolute. iothread=1 on a disk attached to a plain VirtIO SCSI controller does nothing useful. The controller type must be VirtIO SCSI single, which is the type that gives each disk its own controller — and that is what makes “one I/O thread per controller” mean “one per disk”.

It is a concurrency change, not a speed change. Without it, disk I/O completion competes with the guest’s vCPU threads and QEMU’s main event loop. That contention is what an iothread removes. So the benefit appears on guests with several busy disks and several vCPUs under simultaneous load, and is close to zero on a single-disk guest doing sequential work.

Configuration changethe arrangement that actually enables iothreads
set -euo pipefail
VMID=118

# VirtIO SCSI single is the prerequisite, not an optimisation on top.
qm set "$VMID" --scsihw virtio-scsi-single

qm set "$VMID" --scsi0 ceph-vm:vm-118-disk-0,iothread=1,cache=none,aio=io_uring,discard=on

qm shutdown "$VMID" --timeout 300
qm start "$VMID"
Read-only / Safewhat the running machine actually uses
# qm showcmd 118 --pretty | grep -E 'aio=|cache=|iothread|drive'
-drive 'file=rbd:ceph-vm/vm-118-disk-0:conf=/etc/pve/ceph.conf,if=none,id=drive-scsi0,cache=none,discard=on,format=raw,aio=io_uring,detect-zeroes=unmap'
-device 'virtio-scsi-single,id=scsihw0,bus=pci.0,addr=0x5,iothread=iothread-virtioscsi0'

Illustrative output

Measuring, rather than adopting

A tuning change with no measurement is a rumour with a change ticket attached. The measurement does not have to be elaborate, but it does have to run inside the guest, with a working set larger than the host page cache, and it has to be repeated on the unchanged configuration.

Service impact possiblea baseline that is not lying to you
set -euo pipefail

# 4 KiB random writes, queue depth 32, direct I/O to bypass the guest cache.
# The size must exceed the host page cache or you are measuring RAM.
fio --name=randwrite \
  --filename=/var/tmp/fio-test \
  --rw=randwrite --bs=4k --iodepth=32 --numjobs=4 \
  --size=8G --runtime=120 --time_based \
  --direct=1 --ioengine=libaio --group_reporting

# The number that matters for most workloads is not IOPS - it is the
# 99th percentile completion latency, which fio reports as clat percentiles.

rm -f /var/tmp/fio-test

Sensible defaults, and when to leave them

Backendcacheaioiothreaddiscard
Ceph/RBDnoneio_uring1 with virtio-scsi-singleon
ZFS (local)noneio_uring1 with virtio-scsi-singleon
LVM-thinnoneio_uring1 with virtio-scsi-singleon
Directory on local SSDnone, or writeback if power-protectedio_uring1 with virtio-scsi-singleon
NFSnone or writebackio_uring or threads1 with virtio-scsi-singleon for qcow2
Disposable build guestunsafeanyanyon

The honest summary of that table is that cache=none plus io_uring plus iothread on virtio-scsi-single is right almost everywhere, and the interesting decisions are all about when to deviate. If a tuning discussion starts from anywhere else, ask what measurement produced it.

Knowledge check

Knowledge check · 4 questions

  1. Q1. Which combination of aio and cache should be avoided?

  2. Q2. Which conditions must hold for iothread to have a real effect? Select all that apply.

  3. Q3. cache=writeback is a straightforward improvement on ZFS and Ceph because it adds a caching layer the guest did not have.

  4. Q4. A disk benchmark run inside a guest shows a 15% improvement in mean IOPS after a tuning change. What should you check before adopting it?

Passing score: 75%. Answers are checked in this browser.