fio (Flexible I/O Tester) is the standard tool for measuring how fast a disk or volume really is. It lets you control the block size, queue depth, read/write mix and caching behaviour, so you can reproduce the access pattern of a database, a file server or a backup job. In this tutorial you will install fio on Ubuntu 24.04, run the four core benchmarks (random and sequential, read and write), measure latency percentiles, simulate synchronous database writes and save the results as JSON for later comparison.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS or dedicated server.
- A non-root user with
sudoprivileges. - At least 5 GB of free space on the filesystem you want to test.
Warningfio can write directly to block devices such as
/dev/sdbor/dev/nvme1n1. Doing so destroys the data on that device. This guide always tests with a file inside a mounted filesystem, which is safe on a server in use.
Step 1 - Installing fio
fio is packaged in the Ubuntu repositories. Install it together with jq, which you will use later to read the JSON results:
sudo apt update
sudo apt install fio jq
Check the installed version:
fio --version
fio-3.36
Step 2 - Preparing a test directory
fio needs a file to read from and write to. Create a dedicated directory on the filesystem you want to measure. If you want to benchmark a second disk mounted at /mnt/data, create the directory there instead of in your home:
mkdir -p ~/fio-test
cd ~/fio-test
Confirm which device backs that directory and how much space is free:
df -h ~/fio-test
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 78G 6.1G 72G 8% /
Every test below uses these common options, so it helps to know what they do:
| Option | Meaning |
|---|---|
--ioengine=libaio | Linux asynchronous I/O, required for queue depths above 1 |
--direct=1 | Bypass the page cache so you measure the disk, not RAM |
--size=4G | Size of the test file |
--bs | Block size of each request (4k for random, 1M for sequential) |
--iodepth | Number of outstanding requests per job |
--runtime=60 --time_based | Run for exactly 60 seconds |
--group_reporting | Report all jobs as one result |
Step 3 - Measuring random read and write IOPS
Random 4 KiB I/O is the pattern that matters most for databases, virtual machines and busy web applications. Run a random read test with a queue depth of 32:
fio --name=randread --filename=fio.dat --size=4G \
--ioengine=libaio --direct=1 --rw=randread --bs=4k \
--iodepth=32 --numjobs=1 --runtime=60 --time_based --group_reporting
The first run spends a few seconds creating the 4 GB file ("Laying out IO file"). When it finishes, look at the read: line and the clat percentiles block:
randread: (groupid=0, jobs=1): err= 0: pid=4112: Thu Sep 25 10:12:41 2026
read: IOPS=78.9k, BW=308MiB/s (323MB/s)(18.1GiB/60001msec)
clat (usec): min=61, max=5120, avg=403.12, stdev=98.40
clat percentiles (usec):
| 1.00th=[ 239], 5.00th=[ 277], 10.00th=[ 302], 20.00th=[ 330],
| 50.00th=[ 396], 90.00th=[ 506], 95.00th=[ 553], 99.00th=[ 693],
| 99.50th=[ 766], 99.90th=[ 1123], 99.95th=[ 1369], 99.99th=[ 2245]
Now run the same test for random writes:
fio --name=randwrite --filename=fio.dat --size=4G \
--ioengine=libaio --direct=1 --rw=randwrite --bs=4k \
--iodepth=32 --numjobs=1 --runtime=60 --time_based --group_reporting
The result appears under a write: line. On NVMe storage random reads commonly reach tens or hundreds of thousands of IOPS, while a spinning HDD manages only 100 to 200.
To see how the disk scales with parallelism, repeat the read test with --numjobs=4. If IOPS grows, the device has spare capacity and your application benefits from issuing requests in parallel.
Step 4 - Measuring sequential throughput
Sequential reads and writes with large blocks show the maximum bandwidth, which matters for backups, log shipping and media files. Use 1 MiB blocks and a lower queue depth:
fio --name=seqread --filename=fio.dat --size=4G \
--ioengine=libaio --direct=1 --rw=read --bs=1M \
--iodepth=16 --runtime=60 --time_based --group_reporting
read: IOPS=1712, BW=1712MiB/s (1795MB/s)(100GiB/60004msec)
Then the sequential write test:
fio --name=seqwrite --filename=fio.dat --size=4G \
--ioengine=libaio --direct=1 --rw=write --bs=1M \
--iodepth=16 --runtime=60 --time_based --group_reporting
For these tests read the BW= value: IOPS is not meaningful with 1 MiB blocks.
Step 5 - Simulating a database workload
Two patterns approximate real database behaviour better than pure reads or writes.
The first is a mixed random workload, here 70% reads and 30% writes with 16 KiB blocks (the InnoDB page size) and four parallel jobs:
fio --name=oltp --filename=fio.dat --size=4G \
--ioengine=libaio --direct=1 --rw=randrw --rwmixread=70 --bs=16k \
--iodepth=16 --numjobs=4 --runtime=60 --time_based --group_reporting
fio prints separate read: and write: lines for the mix.
The second is synchronous commit latency. A database flushes its write-ahead log with fsync() on every commit, one request at a time. This test issues a 4 KiB write followed by an fsync with a queue depth of 1:
fio --name=fsync-latency --filename=fio.dat --size=1G \
--ioengine=sync --rw=write --bs=4k --fsync=1 \
--runtime=30 --time_based
write: IOPS=2987, BW=11.7MiB/s (12.2MB/s)(350MiB/30001msec)
fsync/fdatasync/sync_file_range:
sync (usec): min=198, max=8421, avg=305.44, stdev=110.21
The IOPS figure here is roughly the maximum number of single-threaded commits per second the disk can sustain. Consumer SSDs without power-loss protection often score very low on this test even when their random IOPS look impressive.
Step 6 - Reading latency percentiles
Averages hide slow outliers, so focus on the percentiles in the clat percentiles block:
50.00this the median: half of all requests completed faster than this.99.00thmeans 99% of requests completed within this time. This is a good figure to compare disks.99.90thand99.99thshow tail latency. Values far above p99 point to throttling, garbage collection on the SSD or contention with other workloads.
clat is the completion latency measured from when the request was submitted to the device. lat includes submission overhead as well; for comparisons, use the same one consistently.
Step 7 - Saving tests in a job file
Typing long command lines is error-prone. A job file keeps the whole suite in one place and makes runs repeatable. Create one:
nano ~/fio-test/suite.fio
Add the following content. The [global] section applies to every job, and stonewall makes each job wait for the previous one to finish so they do not compete:
[global]
filename=fio.dat
size=4G
ioengine=libaio
direct=1
runtime=60
time_based
group_reporting
[randread-4k]
rw=randread
bs=4k
iodepth=32
stonewall
[randwrite-4k]
rw=randwrite
bs=4k
iodepth=32
stonewall
[seqread-1m]
rw=read
bs=1M
iodepth=16
stonewall
[seqwrite-1m]
rw=write
bs=1M
iodepth=16
stonewall
[mixed-70-30]
rw=randrw
rwmixread=70
bs=16k
iodepth=16
numjobs=4
stonewall
Run the suite and write the results as JSON:
cd ~/fio-test
fio suite.fio --output-format=json --output=baseline.json
The suite takes about five minutes.
Step 8 - Extracting results with jq
The JSON output contains one entry per job in the jobs array. Extract IOPS, bandwidth (in KiB/s) and p99 completion latency (in nanoseconds) for each job:
jq -r '.jobs[] | [.jobname,
(.read.iops | floor), (.write.iops | floor),
.read.bw, .write.bw,
(.read.clat_ns.percentile["99.000000"] // 0),
(.write.clat_ns.percentile["99.000000"] // 0)] | @tsv' baseline.json
randread-4k 78921 0 315684 0 692224 0
randwrite-4k 0 41230 0 164920 0 1236992
seqread-1m 1711 0 1752064 0 0 0
seqwrite-1m 0 1189 0 1217536 0 0
mixed-70-30 20411 8752 326576 140032 1073152 1531904
Keep baseline.json. After you change something (a new disk, a different filesystem, a kernel update, a larger plan), run the suite again to after.json and compare the same fields. Always run each test at least twice: on shared or cloud storage results can vary between runs.
Step 9 - Cleaning up
Remove the test file so it does not occupy space:
rm ~/fio-test/fio.dat
Troubleshooting
fio: looks like your file system does not support direct=1/buffered=0: the filesystem (for example tmpfs) does not support O_DIRECT. Test on a real disk-backed filesystem such as ext4 or XFS.
Read results are unrealistically high (several GB/s on a small VPS): --direct=1 is missing, so reads are being served from the page cache. Add it back, or use a test file larger than the server's RAM.
No space left on device: reduce --size or free space on the filesystem. fio needs the full file size available.
Conclusion
You installed fio, measured random IOPS, sequential bandwidth, mixed and synchronous database workloads, and learned to read latency percentiles. With a job file and JSON output you now have a repeatable baseline to compare against after hardware or configuration changes. As next steps, benchmark your database directly with pgbench or sysbench, and watch device utilisation during a test with iostat -x 1 from the sysstat package.
