Skip to content
About
Main content

Queue depth and the 0.7% reader

Measure your machineNeeds Python

Problem

An SSD is a parallel device: it has many flash channels and serves several requests at once. One request in flight leaves most of it idle, so throughput at depth 1 is 1 ÷ ; adding requests in flight raises throughput almost linearly until the device is saturated, at a depth of about its latency times its divided by the request size. A program that reads one block at a time sees only the depth-1 number.

On a laptop, create a 2 GB file and read random 4 blocks with 1, 4, 16, 64 and 128 requests in flight (one thread per request, page cache bypassed), and plot IOPS against the number in flight. Then try a plain mmap read of the same file in random order with one thread.

The question. How does IOPS grow with requests in flight, and where does it flatten?


Predict

As more reads are kept in flight, where does this SSD's IOPS curve flatten?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    queue-depth-and-the-0-7.py58 linesDownload
    View the code
    queue-depth-and-the-0-7.py
    # /// script
    # requires-python = ">=3.10"
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """Queue depth and the device limit.
    
    A 2 GB file read in random 4 KB blocks, bypassing the page cache (F_NOCACHE
    on macOS, posix_fadvise elsewhere), by 1, 4, 16, 64 and 128 threads each with
    one request in flight: IOPS against requests in flight. Then the same random
    reads through mmap with one thread.
    """
    import argparse, json, mmap, os, random, sys, tempfile, threading, time
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--file-mb", type=int, default=2048, help="Test file (MB).")
    p.add_argument("--depths", default="1,4,16,64,128", help="Requests in flight.")
    p.add_argument("--seconds", type=float, default=5, help="Seconds per depth.")
    p.add_argument("--json", help="Also write the results to this JSON file.")
    args = p.parse_args()
    
    path = os.path.join(tempfile.mkdtemp(), "qd.bin")
    with open(path, "wb") as f:
        for _ in range(args.file_mb): f.write(os.urandom(1 << 20))
    blocks = args.file_mb * 256
    def open_nocache():
        fd = os.open(path, os.O_RDONLY)
        if sys.platform == "darwin":
            import fcntl; fcntl.fcntl(fd, fcntl.F_NOCACHE, 1)
        return fd
    def run(depth):
        counts = [0] * depth; stop = time.perf_counter() + args.seconds
        def worker(i):
            fd = open_nocache(); rng = random.Random(i); n = 0
            while time.perf_counter() < stop:
                os.pread(fd, 4096, rng.randrange(blocks) * 4096); n += 1
            counts[i] = n; os.close(fd)
        ts = [threading.Thread(target=worker, args=(i,)) for i in range(depth)]
        for t in ts: t.start()
        for t in ts: t.join()
        iops = sum(counts) / args.seconds
        return {"in_flight": depth, "iops": iops, "mb_s": iops * 4096 / 2**20, "latency_us": depth / iops * 1e6}
    if hasattr(os, "posix_fadvise"):
        fd = os.open(path, os.O_RDONLY); os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED); os.close(fd)
    rows = [run(int(d)) for d in args.depths.split(",")]
    with open(path, "rb") as f:
        m = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ); rng = random.Random(9); n = 0; stop = time.perf_counter() + args.seconds
        while time.perf_counter() < stop: m[rng.randrange(blocks) * 4096]; n += 1
        m.close()
    os.remove(path)
    summary = {"mmap_iops": n / args.seconds, "depth1_iops": rows[0]["iops"], "depth1_latency_us": rows[0]["latency_us"], "peak_iops": max(r["iops"] for r in rows)}
    for r in rows: print(f"{r['in_flight']:>4} in flight: {r['iops']:>9,.0f} IOPS  ({r['mb_s']:7.1f} MB/s, {r['latency_us']:7.1f} µs per read)")
    print(f"mmap, one thread: {summary['mmap_iops']:,.0f} reads/s (pages may come from the cache after the first touch)")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"summary": summary, "tables": {"depths": rows}}, f)
    
  3. 3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.

    Terminal
    uv run queue-depth-and-the-0-7.py
    uv run queue-depth-and-the-0-7.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--file-mb2048 Test file (MB).
--depths1, 4, 16, 64, 128 Requests in flight.
--seconds5 Seconds per depth.

Our run

IOPS against requests in flightApple M2 · 8 cores · 8 GB
Data table
IOPS against requests in flight
Requests in flightRandom 4 KB reads per second
111.8K
437.6K
1672.4K
6475.4K
12875.2K
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20

Random 4 KB reads

Random 4 KB reads
In flightIOPSMB/sµs per read
111.8K46.284.6
437.6K147106
1672.4K283221
6475.4K294849
12875.2K2941,700
Output · uv run queue-depth-and-the-0-7.py
   1 in flight:    11,817 IOPS  (   46.2 MB/s,    84.6 µs per read)
   4 in flight:    37,608 IOPS  (  146.9 MB/s,   106.4 µs per read)
  16 in flight:    72,445 IOPS  (  283.0 MB/s,   220.9 µs per read)
  64 in flight:    75,357 IOPS  (  294.4 MB/s,   849.3 µs per read)
 128 in flight:    75,165 IOPS  (  293.6 MB/s,  1702.9 µs per read)
mmap, one thread: 12,822 reads/s (pages may come from the cache after the first touch)

What we found

With one read in flight the SSD delivered 11.8K IOPS, 84.6 µs per read, almost exactly the expected 1 ÷ latency for an 80 µs device. Four in flight gave 37.6K and sixteen 72.4K; from there it flattened (75.2K at 128) while each read's latency kept growing, to 1,700 µs. Random reads through with one thread ran at 12.8K per second, about the depth-1 rate: each is one read in flight.


Read it

A single-threaded random reader measures the device's latency, not its capacity. Engines that issue reads asynchronously or in parallel (prefetching, , many concurrent queries) get the rest; mmap gets only what page faults, one at a time per thread, can drive.