SSD Benchmarking with fio: Throughput and IOPS, Test-File Only
Want real numbers on your SSD’s speed? fio (Flexible I/O Tester) is the standard tool. This post benchmarks sequential and random read/write performance on a real NVMe SSD, using only regular test files on an existing filesystem: never a raw device path like /dev/sdX or /dev/nvme0n1.
Why test files, not raw devices
Pointing fio at /dev/sdX benchmarks the whole block device directly, bypassing the filesystem entirely. Two problems:
- It’s destructive. fio with
rw=writeagainst a raw device overwrites whatever partition table, filesystem, or data lives there. No undo. - It’s not what your apps see. Your database, container runtime, and app server all read/write through a filesystem. Benchmarking the bare device measures something you don’t actually use.
The safe, realistic approach: create a directory on the target filesystem, point fio’s --filename at a regular file inside it, give it a --size, and let fio create and fill that file itself. Same underlying SSD hardware, same driver stack, zero risk to existing partitions.
Machine used for this run: 16 vCPU, ext4 root filesystem on an NVMe SSD (/dev/nvme0n1p4), fio 3.42.
$ mkdir -p /home/dat/fio-test
$ df -hT /home/dat/data
Filesystem Type Size Used Avail Use% Mounted on
/dev/nvme0n1p4 ext4 938G 725G 165G 82% /
/dev/nvme0n1p4 on / type ext4 (rw,relatime)
The core fio flags
--filename=testfile: a plain file, not a device node--size=2G: how big the test file is; fio creates/pads it as needed--direct=1: bypass the page cache so you measure the disk, not RAM--ioengine=libaio: async I/O engine, needed to drive real queue depth--iodepth=N: how many I/Os in flight at once (1 for sequential, 32 for IOPS tests)--rw=: access pattern (write,read,randwrite,randread,randrw)--bs=: block size, large (1M) for throughput tests, small (4k) for IOPS tests--runtime=+--time_based: run for a fixed wall-clock duration instead of until size is exhausted
Sequential write: throughput
Large blocks, low queue depth: this is the “how fast can it write a big file” number.
$ fio --name=seqwrite --filename=testfile --size=2G --rw=write \
--bs=1M --direct=1 --ioengine=libaio --iodepth=4 --numjobs=1 --group_reporting
seqwrite: (g=0): rw=write, bs=(R) 1024KiB-1024KiB, (W) 1024KiB-1024KiB, (T) 1024KiB-1024KiB, ioengine=libaio, iodepth=4
fio-3.42
Starting 1 process
seqwrite: Laying out IO file (1 file / 2048MiB)
seqwrite: (groupid=0, jobs=1): err= 0: pid=2074955: Sat Jul 25 22:36:56 2026
write: IOPS=4240, BW=4240MiB/s (4446MB/s)(2048MiB/483msec)
slat (usec): min=9, max=126, avg=22.09, stdev= 9.89
clat (usec): min=173, max=51482, avg=918.71, stdev=2364.83
lat (usec): min=188, max=51500, avg=940.81, stdev=2365.06
clat percentiles (usec):
| 1.00th=[ 277], 5.00th=[ 334], 10.00th=[ 404], 20.00th=[ 537],
| 30.00th=[ 644], 40.00th=[ 660], 50.00th=[ 734], 60.00th=[ 791],
| 70.00th=[ 832], 80.00th=[ 881], 90.00th=[ 996], 95.00th=[ 1319],
| 99.00th=[ 5211], 99.50th=[ 6259], 99.90th=[51119], 99.95th=[51119],
| 99.99th=[51643]
bw ( MiB/s): min= 4096, max= 4096, per=96.60%, avg=4096.00, stdev= 0.00, samples=1
iops : min= 4096, max= 4096, avg=4096.00, stdev= 0.00, samples=1
lat (usec) : 250=0.68%, 500=16.65%, 750=36.43%, 1000=36.67%
lat (msec) : 2=6.79%, 4=1.32%, 10=1.12%, 20=0.15%, 100=0.20%
cpu : usr=3.73%, sys=6.43%, ctx=2195, majf=0, minf=10
IO depths : 1=0.1%, 2=0.1%, 4=99.9%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
issued rwts: total=0,2048,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0.00ns, window=0.00ns, percentile=100.00%, depth=4
Run status group 0 (all jobs):
WRITE: bw=4240MiB/s (4446MB/s), 4240MiB/s-4240MiB/s (4446MB/s-4446MB/s), io=2048MiB (2147MB), run=483-483msec
Disk stats (read/write):
nvme0n1: ios=3/6205, sectors=24/3174888, merge=0/121, ticks=0/5195, in_queue=5246, util=70.60%
~4.4 GB/s sequential write. Note util=70.60%: the disk wasn’t even fully saturated at queue depth 4, a fast NVMe drive laughs at 1M sequential writes.
Sequential read: throughput
Same file, read back sequentially.
$ fio --name=seqread --filename=testfile --size=2G --rw=read \
--bs=1M --direct=1 --ioengine=libaio --iodepth=4 --numjobs=1 --group_reporting
seqread: (g=0): rw=read, bs=(R) 1024KiB-1024KiB, (W) 1024KiB-1024KiB, (T) 1024KiB-1024KiB, ioengine=libaio, iodepth=4
fio-3.42
Starting 1 process
seqread: (groupid=0, jobs=1): err= 0: pid=2075216: Sat Jul 25 22:37:00 2026
read: IOPS=6360, BW=6360MiB/s (6669MB/s)(2048MiB/322msec)
slat (usec): min=9, max=265, avg=19.41, stdev=12.07
clat (usec): min=288, max=5089, avg=607.95, stdev=235.65
lat (usec): min=308, max=5326, avg=627.36, stdev=242.09
clat percentiles (usec):
| 1.00th=[ 363], 5.00th=[ 433], 10.00th=[ 474], 20.00th=[ 519],
| 30.00th=[ 537], 40.00th=[ 545], 50.00th=[ 570], 60.00th=[ 594],
| 70.00th=[ 635], 80.00th=[ 652], 90.00th=[ 750], 95.00th=[ 840],
| 99.00th=[ 1270], 99.50th=[ 1942], 99.90th=[ 4080], 99.95th=[ 4080],
| 99.99th=[ 5080]
lat (usec) : 500=13.87%, 750=75.88%, 1000=9.08%
lat (msec) : 2=0.78%, 4=0.20%, 10=0.20%
cpu : usr=0.93%, sys=10.87%, ctx=2046, majf=0, minf=1034
IO depths : 1=0.1%, 2=0.1%, 4=99.9%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
issued rwts: total=2048,0,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0.00ns, window=0.00ns, percentile=100.00%, depth=4
Run status group 0 (all jobs):
READ: bw=6360MiB/s (6669MB/s), 6360MiB/s-6360MiB/s (6669MB/s-6669MB/s), io=2048MiB (2147MB), run=322-322msec
Disk stats (read/write):
nvme0n1: ios=3647/18, sectors=1867264/2920, merge=0/0, ticks=1924/12, in_queue=1935, util=53.41%
~6.7 GB/s sequential read: reads outrun writes here, typical for NVMe (no erase-before-write cost on the read path).
Random write: IOPS
Small blocks (4k), deep queue (32): this is what stresses the SSD’s controller and FTL, the number that matters for databases and VMs doing scattered small I/O.
$ fio --name=randwrite --filename=testfile --size=2G --rw=randwrite \
--bs=4k --direct=1 --ioengine=libaio --iodepth=32 --numjobs=1 \
--runtime=15 --time_based --group_reporting
randwrite: (g=0): rw=randwrite, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=32
fio-3.42
Starting 1 process
randwrite: (groupid=0, jobs=1): err= 0: pid=2075281: Sat Jul 25 22:37:17 2026
write: IOPS=145k, BW=568MiB/s (595MB/s)(8517MiB/15002msec)
slat (nsec): min=490, max=1403.8k, avg=1607.41, stdev=2612.30
clat (usec): min=2, max=100068, avg=218.27, stdev=1414.66
lat (usec): min=4, max=100069, avg=219.88, stdev=1414.67
clat percentiles (usec):
| 1.00th=[ 14], 5.00th=[ 47], 10.00th=[ 59], 20.00th=[ 70],
| 30.00th=[ 91], 40.00th=[ 112], 50.00th=[ 145], 60.00th=[ 151],
| 70.00th=[ 155], 80.00th=[ 167], 90.00th=[ 223], 95.00th=[ 537],
| 99.00th=[ 1450], 99.50th=[ 1827], 99.90th=[ 6587], 99.95th=[49546],
| 99.99th=[51643]
bw ( KiB/s): min=78504, max=811704, per=100.00%, avg=581442.67, stdev=210306.79, samples=30
iops : min=19626, max=202926, avg=145360.67, stdev=52576.70, samples=30
lat (usec) : 4=0.01%, 10=0.88%, 20=1.01%, 50=3.82%, 100=30.78%
lat (usec) : 250=54.51%, 500=3.67%, 750=1.72%, 1000=1.09%
lat (msec) : 2=2.06%, 4=0.29%, 10=0.09%, 20=0.01%, 50=0.03%
lat (msec) : 100=0.04%, 250=0.01%
cpu : usr=5.54%, sys=23.43%, ctx=1279580, majf=0, minf=10
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=100.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.1%, 64=0.0%, >=64=0.0%
issued rwts: total=0,2180410,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0.00ns, window=0.00ns, percentile=100.00%, depth=32
Run status group 0 (all jobs):
WRITE: bw=568MiB/s (595MB/s), 568MiB/s-568MiB/s (595MB/s-595MB/s), io=8517MiB (8931MB), run=15002-15002msec
Disk stats (read/write):
nvme0n1: ios=2/2157112, sectors=16/17283096, merge=0/2034, ticks=60/409444, in_queue=412165, util=86.61%
~145k IOPS random write, 568 MiB/s at 4k block size. Notice the IOPS swing (min 19.6k, max 202.9k across samples): random-write performance on real SSDs isn’t flat, it wobbles with garbage collection and write amplification under the hood.
Random read: IOPS
$ fio --name=randread --filename=testfile --size=2G --rw=randread \
--bs=4k --direct=1 --ioengine=libaio --iodepth=32 --numjobs=1 \
--runtime=15 --time_based --group_reporting
randread: (g=0): rw=randread, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=32
fio-3.42
Starting 1 process
randread: (groupid=0, jobs=1): err= 0: pid=2076295: Sat Jul 25 22:37:38 2026
read: IOPS=46.3k, BW=181MiB/s (190MB/s)(2712MiB/15002msec)
slat (nsec): min=431, max=2239.3k, avg=1779.24, stdev=3344.11
clat (usec): min=121, max=22467, avg=689.12, stdev=361.45
lat (usec): min=124, max=22470, avg=690.90, stdev=361.39
clat percentiles (usec):
| 1.00th=[ 247], 5.00th=[ 379], 10.00th=[ 408], 20.00th=[ 506],
| 30.00th=[ 537], 40.00th=[ 627], 50.00th=[ 652], 60.00th=[ 685],
| 70.00th=[ 766], 80.00th=[ 807], 90.00th=[ 922], 95.00th=[ 1045],
| 99.00th=[ 1663], 99.50th=[ 2245], 99.90th=[ 4948], 99.95th=[ 5866],
| 99.99th=[ 9896]
bw ( KiB/s): min=106800, max=266736, per=100.00%, avg=185162.13, stdev=36913.77, samples=30
iops : min=26700, max=66684, avg=46290.53, stdev=9228.44, samples=30
lat (usec) : 250=1.10%, 500=16.60%, 750=48.73%, 1000=27.83%
lat (msec) : 2=5.04%, 4=0.47%, 10=0.23%, 20=0.01%, 50=0.01%
cpu : usr=2.51%, sys=8.73%, ctx=258909, majf=0, minf=40
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=100.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.1%, 64=0.0%, >=64=0.0%
issued rwts: total=694358,0,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0.00ns, window=0.00ns, percentile=100.00%, depth=32
Run status group 0 (all jobs):
READ: bw=181MiB/s (190MB/s), 181MiB/s-181MiB/s (190MB/s-190MB/s), io=2712MiB (2844MB), run=15002-15002msec
Disk stats (read/write):
nvme0n1: ios=687780/208, sectors=5510584/5008, merge=1043/292, ticks=467187/123, in_queue=467378, util=88.38%
~46.3k IOPS random read: lower than random write here because reads on a fresh, never-written region of the test file can hit different internal paths than writes at qd=32, ratios vary by drive and firmware, which is exactly why you benchmark instead of trusting a spec sheet.
Mixed workload: 70/30 read/write
Real databases don’t do pure read or pure write: --rwmixread=70 simulates a 70% read / 30% write OLTP-style pattern.
$ fio --name=randrw --filename=testfile --size=2G --rw=randrw --rwmixread=70 \
--bs=4k --direct=1 --ioengine=libaio --iodepth=16 --numjobs=1 \
--runtime=10 --time_based --group_reporting
randrw: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=16
fio-3.42
Starting 1 process
randrw: (groupid=0, jobs=1): err= 0: pid=2077436: Sat Jul 25 22:37:57 2026
read: IOPS=76.6k, BW=299MiB/s (314MB/s)(2991MiB/10001msec)
slat (nsec): min=621, max=153239, avg=1754.62, stdev=1319.47
clat (usec): min=10, max=6431, avg=202.75, stdev=162.30
lat (usec): min=12, max=6438, avg=204.50, stdev=162.29
clat percentiles (usec):
| 1.00th=[ 38], 5.00th=[ 45], 10.00th=[ 106], 20.00th=[ 126],
| 30.00th=[ 129], 40.00th=[ 133], 50.00th=[ 143], 60.00th=[ 165],
| 70.00th=[ 210], 80.00th=[ 255], 90.00th=[ 383], 95.00th=[ 506],
| 99.00th=[ 766], 99.50th=[ 1012], 99.90th=[ 1598], 99.95th=[ 1762],
| 99.99th=[ 2409]
bw ( KiB/s): min=163816, max=360760, per=100.00%, avg=306277.60, stdev=47503.17, samples=20
iops : min=40954, max=90190, avg=76569.40, stdev=11875.79, samples=20
write: IOPS=32.8k, BW=128MiB/s (134MB/s)(1283MiB/10001msec)
slat (nsec): min=831, max=116950, avg=2092.88, stdev=1326.43
clat (nsec): min=141, max=1249.6k, avg=7067.69, stdev=5683.62
lat (usec): min=4, max=1251, avg= 9.16, stdev= 6.23
clat percentiles (nsec):
| 1.00th=[ 3632], 5.00th=[ 4256], 10.00th=[ 4384], 20.00th=[ 4640],
| 30.00th=[ 5152], 40.00th=[ 5536], 50.00th=[ 5792], 60.00th=[ 6368],
| 70.00th=[ 7008], 80.00th=[ 8256], 90.00th=[11072], 95.00th=[14272],
| 99.00th=[23168], 99.50th=[28032], 99.90th=[40192], 99.95th=[46848],
| 99.99th=[69120]
bw ( KiB/s): min=69584, max=155096, per=100.00%, avg=131339.20, stdev=20302.93, samples=20
iops : min=17396, max=38774, avg=32834.80, stdev=5075.73, samples=20
lat (nsec) : 250=0.01%, 500=0.01%, 750=0.01%
lat (usec) : 2=0.01%, 4=0.82%, 10=25.39%, 20=3.28%, 50=4.93%
lat (usec) : 100=2.43%, 250=48.73%, 500=10.81%, 750=2.81%, 1000=0.42%
lat (msec) : 2=0.34%, 4=0.01%, 10=0.01%
cpu : usr=6.64%, sys=21.87%, ctx=494746, majf=0, minf=11
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=100.0%, 32=0.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.1%, 32=0.0%, 64=0.0%, >=64=0.0%
issued rwts: total=765694,328348,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0.00ns, window=0.00ns, percentile=100.00%, depth=16
Run status group 0 (all jobs):
READ: bw=299MiB/s (314MB/s), 299MiB/s-299MiB/s (314MB/s-314MB/s), io=2991MiB (3136MB), run=10001-10001msec
WRITE: bw=128MiB/s (134MB/s), 128MiB/s-128MiB/s (134MB/s-134MB/s), io=1283MiB (1345MB), run=10001-10001msec
Disk stats (read/write):
nvme0n1: ios=756215/324460, sectors=6049736/2604424, merge=2/195, ticks=150418/1523, in_queue=151969, util=82.20%
76.6k read IOPS + 32.8k write IOPS concurrently: shows how read and write throughput trade off when they share the same queue depth on the same device.
Watch it run
Terminal recording of an actual fio random-read IOPS test executing against the test file (queue depth 32, 4k blocks, 8-second run):
Results summary
| Test | Pattern | Block size | Queue depth | Result |
|---|---|---|---|---|
| Sequential write | write |
1M | 4 | 4240 MiB/s (4.45 GB/s) |
| Sequential read | read |
1M | 4 | 6360 MiB/s (6.67 GB/s) |
| Random write IOPS | randwrite |
4k | 32 | 145k IOPS, 568 MiB/s |
| Random read IOPS | randread |
4k | 32 | 46.3k IOPS, 181 MiB/s |
| Mixed 70/30 | randrw |
4k | 16 | 76.6k read + 32.8k write IOPS |
Cleanup
Test files live in a normal directory: delete when done, nothing device-level to undo.
$ rm -rf /home/dat/fio-test
Takeaways
- Always
--direct=1: without it you’re benchmarking page cache, not the SSD. - Large block + low queue depth (
bs=1M,iodepth=4) gives throughput numbers. - Small block + high queue depth (
bs=4k,iodepth=32) gives IOPS numbers. --time_based --runtime=Ngives stable, comparable results for random workloads; sequential jobs can just run to completion.- Test files on the target filesystem measure exactly what your applications experience. Raw device paths (
/dev/sdX) are unnecessary for this and destructive if misused, skip them.