Linux disk performance benchmarking
Storage benchmarking measures three fundamental metrics: IOPS (random access), bandwidth (sequential throughput), and latency (response time). The right tool and parameters depend on what you are measuring and why. fio is the standard for serious benchmarking; dd and hdparm serve quick checks.
IOPS vs bandwidth vs latency
| Metric | Measures | Critical for | Typical values |
|---|---|---|---|
| IOPS | Random access operations/sec | Databases, virtualization, transactional workloads | HDD: 80-200, SATA SSD: 80k-100k, NVMe: 200k-500k+ |
| Bandwidth | Sequential data transfer rate | Backups, media streaming, large file transfers | HDD: 150-250 MB/s, SATA SSD: 500-560, NVMe: 3-7 GB/s |
| Latency | Time per I/O operation | Databases, interactive apps, real-time systems | HDD: 5-15ms, SATA SSD: 0.1-0.5ms, NVMe: 0.05-0.2ms |
fio (Flexible I/O Tester)
fio is the standard for storage benchmarking. It simulates configurable I/O workloads with precise control over block size, queue depth, I/O engine, and read/write patterns. Use direct=1 to bypass the page cache; filesystem caches like ZFS ARC will otherwise serve reads from RAM.
Command-line examples
# Random read IOPS (4K, queue depth 256, 4 jobs, 60s)
fio --name=randread --filename=/dev/sda --rw=randread --bs=4k \
--direct=1 --ioengine=libaio --iodepth=256 --numjobs=4 \
--time_based --runtime=60 --group_reporting
# Sequential write bandwidth (1M blocks)
fio --name=seqwrite --filename=/dev/sda --rw=write --bs=1M \
--direct=1 --ioengine=libaio --iodepth=64 --size=5G \
--numjobs=4 --group_reporting
# Mixed 70/30 read/write
fio --name=mixed --filename=/dev/sda --rw=randrw --rwmixread=70 \
--bs=4k --direct=1 --ioengine=libaio --iodepth=128 \
--size=10G --numjobs=4 --time_based --runtime=60 --group_reportingJob file format
[global]
ioengine=libaio
direct=1
iodepth=16
bs=4k
size=1G
numjobs=4
time_based
runtime=60
group_reporting
[rand-read]
rw=randread
filename=/dev/sda
[seq-write]
rw=write
bs=1M
iodepth=64
filename=/dev/sdaKey parameters
| Parameter | Effect |
|---|---|
rw | read, write, randread, randwrite, randrw |
bs | Block size (4k for databases, 1M for sequential) |
direct=1 | Use O_DIRECT, bypass page cache (essential for hardware benchmarks) |
ioengine | libaio (Linux async), io_uring (modern, recommended for NVMe) |
iodepth | Queue depth, outstanding I/O requests (SSD: 32-256, HDD: 1-8) |
numjobs | Number of parallel threads/processes |
time_based + runtime | Run for a duration rather than a data size |
group_reporting | Aggregate stats across all jobs |
Interpreting fio output
- IOPS: random access performance
- Bandwidth (MB/s or GB/s), sequential throughput
- Latency:
slat, submission latency (time to submit I/O)clat, completion latency (time to complete)lat, total latency (slat + clat)
dd (basic sequential test)
# Write 10 GB with direct I/O
dd if=/dev/zero of=testfile bs=1M count=10000 oflag=direct conv=fdatasync
# Read test with direct I/O
dd if=testfile of=/dev/null bs=1M iflag=directdd is sequential only, single-threaded, with no IOPS measurement or queue depth control. It gives a rough sequential throughput number but does not represent real workloads. Use fio for any serious benchmarking.
iostat (extended statistics)
# Extended stats every 1 second
iostat -x 1
# Every 5 seconds, 10 times, specific device
iostat -x 5 10 -p sda
# Human-readable
iostat -h 1| Column | Meaning |
|---|---|
r/s, w/s | Read/write requests per second |
rkB/s, wkB/s | Kilobytes read/written per second |
await | Average time (ms) per I/O, includes queue + service time |
r_await, w_await | Read/write wait time separately |
aqu-sz | Average queue length |
%util | Percentage of time device was busy (saturation indicator) |
Identifying bottlenecks: high %util + high await = device saturated. High await with low %util may indicate controller or network bottleneck. That includes network-attached backends like iSCSI and NVMe-oF, where the fabric adds latency the disk never sees.
iotop (per-process I/O)
# Show only processes doing I/O
iotop -o
# Batch mode for logging
iotop -b -n 10 > iotop.log
# Accumulated I/O since start
iotop -aRequires kernel options CONFIG_TASK_DELAY_ACCT, CONFIG_TASK_IO_ACCOUNTING, CONFIG_TASKSTATS.
Other tools
hdparm (quick read test)
# Buffered disk reads
hdparm -t /dev/sda
# Cache reads (shows RAM speed)
hdparm -T /dev/sda
# Direct I/O (bypass cache)
hdparm -t --direct /dev/sda
# Both
hdparm -tT /dev/sdabiolatency (eBPF, kernel latency histogram)
# Block I/O latency histogram
biolatency
# Per-device histograms
biolatency -D
# Include OS queued time
biolatency -Q
# 1-second summaries, 10 times
biolatency 1 10ioping (latency test)
# Basic latency
ioping .
# Disk seek rate (IOPS)
ioping -R /dev/sda
# Sequential speed (bandwidth)
ioping -RL /dev/sda
# Direct I/O
ioping -D .Best practices
- Always bypass cache. Use
direct=1in fio oroflag=directin dd. Without it you are benchmarking RAM, not storage. - Warm up: run tests multiple times, since SSDs may behave differently after initial writes.
- Match block size to workload: 4k for databases, 64k-256k for virtualization, 1M for backups.
- Use realistic queue depths. HDDs: 1-8, SATA SSDs: 32, NVMe: 128-256.
- Multiple runs: run 3-5 times, take the average, discard outliers.
- Watch for thermal throttling. NVMe drives can exceed 80°C under sustained load and throttle performance, so monitor temperature during long tests.
- Test on the target device, not a file. Testing on a file in another filesystem adds overhead and may not reflect raw device performance.
Matching benchmarks to workloads
| Workload | Block size | Pattern | Queue depth |
|---|---|---|---|
| Database (OLTP) | 4k-8k | randrw (70/30) | 64-256 |
| Virtualization | 64k-256k | randrw | 32-64 |
| Backup / archive | 1M | sequential | 32-64 |
| Web server | mixed | randread | 16-64 |
| File server | mixed | mixed | 16-32 |
Run TRIM on a used SSD before benchmarking so garbage collection isn't mid-cycle. If the device sits behind a cache layer, measure under real load (see storage tiering and caching for cache effectiveness).
Sources & references
- fio — Flexible I/O Tester documentation — official fio documentation covering job file format, I/O engines, parameters, and output interpretation
- axboe/fio GitHub repository — official fio source repository by Jens Axboe with authoritative documentation and examples
- iostat(1) — Linux manual page — man page for iostat documenting extended statistics columns for monitoring device saturation
- open(2) — Linux manual page (O_DIRECT) — documentation for O_DIRECT flag explaining page cache bypass, essential for hardware benchmarking with fio direct=1
Frequently asked questions
How do I benchmark random read IOPS with fio?
Run `fio --name=randread --filename=/dev/sda --rw=randread --bs=4k --direct=1 --ioengine=libaio --iodepth=256 --numjobs=4 --time_based --runtime=60 --group_reporting`. Use `direct=1` to bypass the page cache.
Why use direct=1 in fio?
`direct=1` uses O_DIRECT to bypass the page cache. Without it you are benchmarking RAM, not storage. Always use `direct=1` (or `oflag=direct` in dd) for hardware benchmarks.
Is dd a good disk benchmark?
No. dd is sequential only, single-threaded, with no IOPS measurement or queue depth control. It gives a rough sequential throughput number but does not represent real workloads. Use fio for serious benchmarking.
What iostat values indicate a storage bottleneck?
High `%util` combined with high `await` means the device is saturated. High `await` with low `%util` may indicate a controller or network bottleneck.
What block size should I use for benchmarking?
Match the block size to your workload: 4k for databases, 64k-256k for virtualization, and 1M for backups or sequential throughput tests.
What queue depth should I use for benchmarking?
Use realistic queue depths: 1-8 for HDDs, 32 for SATA SSDs, and 128-256 for NVMe drives. `iodepth` controls the number of outstanding I/O requests.
What fio latency metrics should I look at?
fio reports `slat` (submission latency), `clat` (completion latency), and `lat` (total latency, slat + clat). These show how long I/O takes to submit and complete.