LinuxXLI · Storage PerformanceQueue depth
Queue depth and utilisation - storage saturation signals
What you'll learn
- Explain storage queue depth as concurrency, not as a saturation threshold
- Read aqu-sz in iostat and interpret it against the device queue capability
- Distinguish queue depth from utilisation
- Select and persist a blk-mq I/O scheduler, and tune read-ahead
- Explain why nr_requests is a software queue and not a throughput knob
Prerequisites
Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09
Queue depth is the number of in-flight I/O operations. It is a measure of concurrency, not of health. A deep queue on a device built for parallelism is normal; the same depth on a single spinning disk is a latency problem. The number alone never tells you which.
Queue depth explained
When an application submits an I/O, the kernel queues it on the device’s request queue. The device’s driver processes the queue, dispatching requests to the device.
Queue depth:
- 1: only one I/O at a time. Low throughput, low latency.
- 32: typical default for SATA.
- 256+: high queue depth, used for high-throughput workloads (databases, batch jobs).
Higher queue depth = more requests in flight = potentially higher throughput. But also potentially higher latency (requests wait longer).
Read aqu-sz
iostat -x 1
Output:
Device r/s w/s await aqu-sz %util
sda 5 10 2.50 0.50 5.00
nvme0n1 100 50 0.50 1.50 30.00
aqu-sz is the time-averaged number of requests in flight. It
follows Little’s Law:
aqu-sz ≈ IOPS × await
So aqu-sz rises whenever either the workload submits more
concurrent I/O or the device gets slower. It is one number
covering two very different causes, which is exactly why a fixed
threshold cannot interpret it.
Interpret aqu-sz against the device’s queue capability, and
always alongside await:
aqu-sz20 on a single SATA disk (NCQ 32): the queue is nearly full.awaitwill already be climbing. Real queueing.aqu-sz20 on an NVMe with many hardware queues: ordinary. Check whetherawaitmoved at all before calling it anything.aqu-sz0.3 withawaitof 40 ms: barely any concurrency and terrible latency. The device is slow, not saturated - a failing disk, a throttled cloud volume, or a noisy neighbour.
Saturation is the knee of the latency curve: the point where
offered load keeps rising, IOPS and throughput stop rising, and
await climbs instead. You find it by watching IOPS and await
together over time, not by comparing aqu-sz with a constant.
Distinguish from utilisation
%util is the percentage of elapsed time during which at least
one request was in flight. It measures busy, not full.
aqu-sz is how many requests were in flight on average.
- High
%util, lowaqu-sz: one request at a time, and the device was rarely idle. On a serial device that is genuinely all it can do. - Low
%util, highaqu-sz: near-impossible by definition, because requests in flight are what make the device count as busy. Treat it as a sampling artefact, not a finding. - High both: read
awaitbefore concluding anything. On a serial device it is saturation; on a parallel device it is ordinary concurrent load.
The block layer: scheduler, nr_requests, read-ahead
Before you touch any knob, know which layer it belongs to. There are three distinct queues between an application and the platter or flash:
| Layer | Knob | What it controls |
|---|---|---|
| Block layer scheduler | /sys/block/DEV/queue/scheduler | Which algorithm orders and merges requests |
| Scheduler queue depth | /sys/block/DEV/queue/nr_requests | How many requests the software queue holds |
| Device queue depth | /sys/block/DEV/device/queue_depth (SCSI) | How many requests the hardware accepts at once |
Which scheduler is active
# Brackets mark the current scheduler
cat /sys/block/nvme0n1/queue/scheduler # [none] mq-deadline kyber bfq
cat /sys/block/sda/queue/scheduler # none [mq-deadline] kyber bfq
# Is this device rotational?
cat /sys/block/sda/queue/rotational # 1 = spinning disk, 0 = SSD/NVMe
All current kernels use the multi-queue block layer (blk-mq).
The single-queue schedulers cfq and deadline are gone. What
you will see is:
none- no reordering at all. The right answer for NVMe and for anything behind a capable hardware RAID controller or a cloud hypervisor. Scheduling costs CPU and adds latency, and the device already reorders internally.mq-deadline- deadline-based, with a read preference and an expiry for starved requests. The sensible default for SATA SSDs and spinning disks, and the usual choice under a database.kyber- targets a latency budget by throttling submission. Useful on fast devices with mixed read/write.bfq- fair-share proportional scheduling per process. Good for interactive desktops and for isolating a noisy tenant; its overhead and its latency variance make it a poor fit under a throughput-sensitive database.
Read-ahead
cat /sys/block/sda/read_ahead_kb # default 128
Read-ahead is how far the kernel reads past a detected sequential
pattern. 128 KB is a compromise. A large sequential scan - a
backup read, an analytics table scan - benefits from raising it;
a random-access OLTP workload gains nothing and wastes page cache
and bandwidth on data it will never read. Measure with iostat -x
before and after: sequential gains show up as higher rkB/s at
the same or lower await.
Persist the choice with udev, not a boot-time echo
A value written to /sys is lost at reboot, and a device added
later never gets it. Set it as a device property instead:
# /etc/udev/rules.d/60-ioscheduler.rules
ACTION=="add|change", KERNEL=="nvme[0-9]n[0-9]", ATTR{queue/scheduler}="none"
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline"
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="mq-deadline", ATTR{queue/read_ahead_kb}="1024"
Reload with sudo udevadm control --reload and re-trigger with
sudo udevadm trigger --subsystem-match=block.
nr_requests is not the device queue depth
# View current
cat /sys/block/sda/queue/nr_requests
# Change it (runtime only; use udev to persist)
echo 256 | sudo tee /sys/block/sda/queue/nr_requests
Detect saturation
iostat -x 1 5
Saturation is a trend across samples, never a single reading. Look for all three together:
aqu-szrising, andawaitrising with it, and- IOPS (
r/s+w/s) flat or falling while the first two rise.
That combination is the knee: more work is being offered, none of
it is completing any faster, so it queues. If aqu-sz rises and
await stays flat, the workload simply became more concurrent
and the device absorbed it - that is capacity being used, not
capacity being exhausted.
Common patterns
| Pattern | Reading |
|---|---|
aqu-sz rising, await rising, IOPS flat | Saturated. The device is the bottleneck |
aqu-sz rising, await flat, IOPS rising | More concurrency, absorbed cleanly. Not a problem |
aqu-sz low, await high | Slow device, not a queue. Failing disk, throttled cloud volume, noisy neighbour |
High %util, low aqu-sz | One I/O at a time and the device is always busy - typical of a rotational disk under a serial workload |
High %util, high aqu-sz, await flat | On a parallel device %util 100 means only “not idle”. Ignore it and judge on await |
Spiky await | Intermittent I/O (backup, SSD garbage collection, scrub) |
Knowledge check
Knowledge check · 5 questions
Q1. What does aqu-sz in iostat measure?
Q2. Storage saturation is diagnosed by await rising while IOPS stops rising, not by aqu-sz crossing any fixed number.
Q3. Which of the following indicate storage saturation? Select all that apply.
Q4. An NVMe database host shows aqu-sz 24, await 0.35 ms, 90k IOPS and %util 99%. The application team reports no latency complaints. What is the correct action?
Q5. Which I/O scheduler is normally correct for an NVMe device, and why?
Passing score: 75%. Answers are checked in this browser.