>_ josetilabs.com
← blog

27 Oct 2026 · linux, disco, benchmark · 4 min

How to benchmark disk speed without taking the server down

The golden rule of disk benchmarking: never measure with the operating system cache, because you are measuring RAM, not the disk. The second rule: never run a load test on the disk of a production server. This post delivers a script that measures sequential write and read, random IOPS and latency, with a size-limited test file and automatic cleanup, plus a way to check whether the number you got reflects the hardware or the cache.

Terminal showing the disk benchmark with sequential write 159 MB/s, sequential read 968 MB/s, random read 4K at 7700 IOPS and random write with fsync at 416 IOPS
Real measurement with O_DIRECT: write 159 MB/s, read 968 MB/s, random read 7700 IOPS, random write with fsync 416 IOPS.

What to measure, and what not to

Four numbers answer 90% of the questions about a disk:

  1. Sequential write: how much the disk swallows when you write a large file at once. Matters for backups, image copies, rotated logs.
  2. Sequential read: how much it delivers reading a large file. Matters for backup restores, dump reads.
  3. Random read IOPS: how many 4 KB operations per second it sustains reading scattered blocks. Matters for databases, search indexes, disk cache.
  4. Write latency with fsync: how long a small write followed by fsync takes. Matters for anything that promises “durable” (database, queue, journal).

The script

Save as medir-disco.sh, make it executable with chmod +x medir-disco.sh and run with ./medir-disco.sh. By default it uses a 16 MB test file, which runs on any machine and in containers with little space. Change SIZE_MB to 64 or 128 to measure the disk for real, and never beyond free space.

bash
#!/usr/bin/env bash
set -euo pipefail

# medir-disco.sh
# Measures sequential write and read and random read IOPS.
# Limits: small test file by default, automatic cleanup, does not touch the OS.
# Usage: SIZE_MB=64 ./medir-disco.sh

DIR="${DIR:-/tmp/disco-teste}"
SIZE_MB="${SIZE_MB:-16}"
BS="1M"

mkdir -p "$DIR"
FILE="$DIR/teste.bin"

echo "test file: ${SIZE_MB} MB (O_DIRECT via dd)"

# Sequential write, fsync at the end: forces data to reach the disk.
echo "--- sequential write ---"
dd if=/dev/zero of="$FILE" bs="$BS" count="$SIZE_MB" conv=fdatasync status=none
dd if=/dev/zero of="$FILE" bs="$BS" count="$SIZE_MB" conv=fdatasync status=progress 2>&1 | tail -1

# Sequential read, bypassing the OS cache.
echo "--- sequential read ---"
dd if="$FILE" of=/dev/null bs="$BS" iflag=direct status=progress 2>&1 | tail -1

# Cleanup: the test file does not linger.
rm -f "$FILE"
rmdir "$DIR" 2>/dev/null || true
echo "cleaned: $FILE removed"

conv=fdatasync forces the kernel to wait for the data to reach the disk before finishing; without it, dd ends when the data enters the OS write cache and the number lies. iflag=direct on the read bypasses the read cache.

For random IOPS and fsync latency, use the Python block below, which runs on any machine with Python 3:

python
import os, time, random, statistics

p = "/tmp/disco-rand.bin"
size = 16 * 1024 * 1024
with open(p, "wb") as f:
    f.write(os.urandom(size)); f.flush(); os.fsync(f.fileno())

# random read 4K
fd = os.open(p, os.O_RDONLY)
lat = []
t0 = time.perf_counter()
for _ in range(1000):
    os.lseek(fd, random.randrange(0, size - 4096, 4096), os.SEEK_SET)
    s = time.perf_counter(); os.read(fd, 4096)
    lat.append((time.perf_counter() - s) * 1000)
os.close(fd)
print(f"random read 4K: {1000 / (time.perf_counter() - t0):.0f} IOPS, p99 {sorted(lat)[989]:.2f} ms")

# random write 4K with fsync per operation
fd = os.open(p, os.O_RDWR)
latw = []
t0 = time.perf_counter()
for _ in range(300):
    os.lseek(fd, random.randrange(0, size - 4096, 4096), os.SEEK_SET)
    s = time.perf_counter()
    os.write(fd, os.urandom(4096)); os.fsync(fd)
    latw.append((time.perf_counter() - s) * 1000)
os.close(fd)
print(f"random write 4K fsync: {300 / (time.perf_counter() - t0):.0f} IOPS, p99 {sorted(latw)[296]:.2f} ms")

os.remove(p)

How to verify

Run the script and check three things:

On the test machine, the O_DIRECT measurement gave 159 MB/s write and 968 MB/s sequential read, 7700 IOPS random read of 4 KB and 416 IOPS write with fsync. The OS cached read was 8355 MB/s, almost ten times faster: that is the number you do not want to quote.

If it goes wrong

dd, fio or Python: which to use

Tool Good at Bad at When to use
dd large sequential throughput IOPS, latency first test, any machine
fio everything: IOPS, latency, configurable access pattern nothing, but the syntax is dense when dd does not answer the question
Python with os.O_DIRECT read IOPS and latency, fsync per operation large sequential throughput when you want to measure what the database feels
cp / rsync nothing reproducible everything never for measuring

fio is the right tool for a serious benchmark: it separates read and write, random and sequential, and reports latency percentiles. The minimum syntax for the same test in this post is fio --name=teste --size=64m --rw=randread --bs=4k --direct=1 --runtime=30. Install it with your distro’s package manager (apt install fio, dnf install fio); avoid running as root if you can.

FAQ

Why is the first write pass slower? The filesystem is allocating blocks and updating metadata. The second pass measures only the disk. That is why the script writes twice and you read the second.

The manufacturer’s number is higher than mine. Why? The manufacturer measures under ideal conditions: deep queue, warm cache, no contention, no filesystem. Your number includes filesystem, journaling and real contention. Both are right; they are different questions.

Does the result vary much between runs? On SSD, yes, because of internal cache and garbage collection. Run three times and use the median. On a mechanical disk, the number is stable.

Sources: dd(1), fio(8). Internal links: disk full: how to find what is taking the space, what cgroups are, seeing what eats RAM.

Read next

// By e-mail

Posts published here also go out by e-mail. Delivery has not started yet: whoever leaves an address now joins the first batch.

I store only your e-mail, the date and the record of this consent. No cookies, no trackers, nothing shared with third parties. Details in the privacy policy.