Hands-on posix_fadvise SEQUENTIAL vs RANDOM vs NORMAL lab on localhost: cold and warm 256MiB reads after drop_caches with real measured p50 MB/s and RPS.
6 min read
posix_fadvise lets you whisper SEQUENTIAL, RANDOM, or NORMAL access patterns to the kernel. Folklore: sequential hints boost readahead; random hints stop it from wasting I/O. How much shows up on a cold 256 MiB file after drop_caches?
This is a hands-on lab with measured numbers:
drop_caches between cold runs (sudo worked on this box).Related links:
Lab honesty (1 Oct 2026 IST): Shared Linux lab box (8 cores, kernel 6.12, overlay/virtio). Python 3.13.5 os.posix_fadvise. File under evidence dir. Affiliates: 0. Storage is not a bare-metal NVMe paper — deltas matter more than absolute GB/s.
Verdict up front: on cold sequential reads, FADV_RANDOM cut throughput to ~989 MB/s vs NORMAL ~1685 MB/s (1.7×). FADV_SEQUENTIAL did not beat NORMAL here (1528 MB/s). On cold random 4 KiB I/O, RANDOM edged NORMAL (~1.15× RPS).
| Advice | Intent (simplified) |
|---|---|
POSIX_FADV_NORMAL | default readahead policy |
POSIX_FADV_SEQUENTIAL | expect forward scans — more readahead |
POSIX_FADV_RANDOM | expect seeks — less readahead |
Hints are advisory. The kernel may ignore them; SSD firmware and page cache muddy the story further.
Related links:
Script: lab-evidence/38-posix-fadvise/results/run_lab.py.
| Advice | p50 wall | MB/s p50 |
|---|---|---|
| NORMAL | 152 ms | 1685 |
| SEQUENTIAL | 168 ms | 1528 (~0.91× NORMAL) |
| RANDOM | 259 ms | 989 (~0.59× NORMAL) |
Wrong hint hurts. Marking a full-file scan as RANDOM cost ~1.7× vs NORMAL. SEQUENTIAL was not a free win over NORMAL on this overlay/virtio stack — quote that honestly.
Related links:
| Advice | p50 | MB/s |
|---|---|---|
| NORMAL | 50.4 ms | 5083 |
| SEQUENTIAL | 48.8 ms | 5244 |
| RANDOM | 51.6 ms | 4962 |
Once warm, advice is noise (± a few percent). Do not demo fadvise on a hot cache and claim victory.
Cold (1500 ops, drop_caches each repeat):
| Advice | p50 | RPS | op p50 |
|---|---|---|---|
| NORMAL | 38.7 ms | 38725 | 18.0 µs |
| SEQUENTIAL | 35.5 ms | 42212 | 19.0 µs |
| RANDOM | 33.6 ms | 44711 | 18.0 µs |
RANDOM / NORMAL RPS ≈ 1.15× — modest, real.
Warm (2000 ops): all land ~0.5–0.6M RPS with ~1.5–1.7 µs op p50 — cache serves everything.
Related links:
drop_caches (or O_DIRECT) when claiming cold behavior.drop_caches or direct I/O).Related links:
File created once (256 MiB), fsyncd. Each cold trial: sync + echo 3 > /proc/sys/vm/drop_caches, short settle, open(O_RDONLY), posix_fadvise(0, size, advice), then the read loop. Warm trials prime with a NORMAL full scan first. MB/s = bytes / p50 wall; random arms also report RPS and per-op percentiles.
Cold numbers answer “what happens when the kernel must fetch blocks.” Warm numbers answer “am I measuring the CPU copy loop?” Shipping only warm sequential ~5 GB/s would make every fadvise advice look identical and teach the wrong lesson. Shipping only cold without saying drop_caches ran would make the lab unreproducible on a machine where caches stay hot between trials.
On this overlay/virtio volume the absolute cold MB/s is still “fast storage” — the RANDOM vs NORMAL ~1.7× gap is the portable takeaway for sequential scanners that accidentally set the wrong hint.
If drop_caches is denied in your environment, say so and switch to a fresh file on a tmpfs-unfriendly path, or document warm-only limits like we did in older page-cache-sensitive labs.
Pair this with the mmap/O_DIRECT lab when you need to bypass page cache entirely, and with ionice when competing writers drown readahead. fadvise is a hint to the cache layer — not a scheduler class and not a durability barrier.
os.posix_fadvise / POSIX_FADV_*payload.bin under the evidence directorysudo drop_caches (echo 3) successfully between trialsBottom line for operators: treat FADV_RANDOM as a seatbelt for seek-heavy readers, not as something to sprinkle on every file descriptor. A misplaced RANDOM hint on a scanner is the failure mode this lab actually caught.
On this box, FADV_RANDOM on a cold sequential scan dropped throughput from ~1685 MB/s (NORMAL) to ~989 MB/s. FADV_SEQUENTIAL did not beat NORMAL on that cold scan. Random 4 KiB cold I/O saw a smaller ~1.15× RPS nod to RANDOM. Hint the access you actually do — and measure cold.
Evidence path on the lab box: lab-evidence/38-posix-fadvise/results/. Affiliates: 0.
Lab evidence
Lab 1 Oct 2026 IST. Python 3.13.5; 256 MiB file; sudo drop_caches between cold runs. Cold sequential 1 MiB reads: NORMAL 1685 MB/s; SEQUENTIAL 1528 (~0.91x); RANDOM 989 (~0.59x vs NORMAL, NORMAL/RANDOM ~1.7x). Warm sequential all ~5.0-5.2 GB/s. Cold random 4 KiB x1500: RANDOM 44711 RPS vs NORMAL 38725 (~1.15x). Warm random ~0.5-0.6M RPS. Affiliates: 0. Evidence: lab-evidence/38-posix-fadvise/.
Hands-on itertools.islice vs list slice window lab: real ops/s taking ranges from sequences, measured on Linux localhost in this hands-on lab for SREs.
Lab1 Oct 20264 min read
Hands-on heapq.merge vs sorted(chain) multi-way merge: real records/s on pre-sorted lists, measured on Linux localhost today in this hands-on lab for SREs.
Lab1 Oct 20264 min read
Hands-on mmap MAP_SHARED write+msync vs pwrite region update: real MB/s with durability labels, measured on Linux localhost in this hands-on lab for SREs.
Lab1 Oct 20264 min read