System design / Search & storage

Disk I/O and Durability for System Design: Page Cache, fsync and Why "Saved" Isn't Saved

What really happens between write() and the disk: the page cache, dirty pages, fsync and directory fsync, HDD vs SSD, journaling filesystems, the 2018 fsyncgate bug, O_DIRECT, epoll vs io_uring, and how grep, find, locate and plocate search, with the durability trade-offs to name in an HLD interview.

On this page
  1. "Saved" has three meanings
  2. Disks 101: HDD vs SSD, sequential vs random
  3. What write() and fsync() really do
  4. Journaling filesystems: a write-ahead log inside the kernel (members only)
  5. fsyncgate: when fsync said yes and meant no (members only)
  6. Skipping the page cache: O_DIRECT (members only)
  7. Waiting for I/O: epoll vs io_uring (members only)
  8. How grep, ripgrep, find, locate and plocate search (members only)
  9. Real systems: durability dials in the wild (members only)
  10. How to say it in the HLD interview (members only)

In 2018, Postgres developers found that Linux could quietly drop a failed disk write, and a later fsync would still report success. The database thought your data was safe. It wasn't. That bug, nicknamed fsyncgate, is the best way I know to show that "saved" means less than most engineers think.

Where this fits: Part of the Search & storage series. The last stop of every query and every write is the disk. This blog opens that box: what Linux does between your write() and the storage media, and how tools like grep and locate search it.

Before you start: Search, end to end is enough.

After reading this you will be able to

  • Say exactly where your data is when write() returns, and when fsync returns.
  • Explain why sequential writes beat random ones on both spinning disks and SSDs.
  • Save a file safely, including the directory fsync most people forget.
  • Tell the fsyncgate story and the lesson databases took from it.
  • Answer "is this write durable?" in an HLD interview without hand-waving.

For this blog, we'll stay on Linux and on a single machine. Networks and replicas come later, in Reads and writes.

01"Saved" has three meanings

Think about sending a letter. You write it, drop it in the office out-tray for a clerk to collect whenever they pass, and later a courier gets a signature at the other end. "Is it sent?" depends on which step you mean. Only the signed delivery survives the office burning down.

Files work the same way. Some words first:

  • Page cache: RAM the kernel uses to hold file data.
  • Dirty page: a page of file data changed in RAM but not yet written to the disk.
  • write(): the system call that hands bytes to the kernel.
  • fsync(): asks the kernel to push a file's data and metadata to the device and wait. fdatasync() skips metadata that isn't needed to read the data back.
  • Durability: once the system says "saved", the data survives a crash or power cut.
  • Journal: a small log the filesystem writes first, so it can repair itself after a crash.

The PostgreSQL reliability docs list the caches your bytes pass on the way down: the OS cache, the disk controller's cache and the drive's own cache. Here is the stack.

Fig. 1

Between write() and the storage media sit many layers of memory

Between write() and the storage media sit many layers of memoryA vertical stack from top to bottom: the app buffer, the write() system call, the kernel page cache holding dirty pages, the filesystem and its journal, the disk controller cache, the drive cache, and finally the storage media. write() returns once data is in the page cache. fsync() returns once the device says the data is on the media, if the hardware tells the truth.Your app bufferbytes in your processwrite() system callhand bytes to the kernelPage cachekernel RAM, dirty pagesFilesystem + journalext4 decides where blocks goDisk controller cacheRAM on the controllerDrive cacheRAM inside the driveStorage mediaplatter or flash: survives power losswrite() returns heredata is only in RAMfsync() returns hereif the hardwaretells the truthOrange: volatile, lost on power cutGreen: durable
Notice how many layers sit between "OK" and the disk. Each one can hold your data in memory that forgets it on power loss.

02Disks 101: HDD vs SSD, sequential vs random

A hard disk drive (HDD) is a spinning platter with a head on a moving arm, like a record player. Moving the arm to a new spot is a seek, about 8-10 ms (Norvig's table says 8 ms). Reading 1 MB in a straight line takes about 20 ms, so by our rough arithmetic one seek costs as much as reading 0.4 MB.

A solid-state drive (SSD) has no moving parts. Colin Scott's latency model puts an SSD random read at about 16 µs. By our own rough arithmetic, that's roughly 500x faster than an 8 ms seek. But as Emmanuel Goossaert explains, flash can't overwrite a small piece in place:

  • It writes in pages but erases only in much bigger blocks.
  • A flash translation layer (FTL) inside the drive remaps every write to a fresh page.
  • Garbage collection copies live pages out of a block so it can be erased; wear levelling spreads writes evenly. That copying is write amplification: one write from you becomes several inside the drive.

Side by side:

Fig. 2

Spinning disks hate seeks; SSDs hate small in-place rewrites

Spinning disks hate seeks; SSDs hate small in-place rewritesLeft: a hard disk with a spinning platter and a head on a moving arm. Every jump to a new spot costs a seek of about 8 to 10 milliseconds. Right: an SSD. Data is written in pages but erased only in whole blocks, and a flash translation layer remaps writes. A random read is about 16 microseconds, but rewriting small pieces causes extra copying.Hard disk (HDD)Solid-state drive (SSD)Spinning platterdata lives on circular tracksHead on a moving armmust travel to each new spotPageswrite unitBlockserase unitFlash translation layerremaps writes, garbage collectsSeek: about 8-10 msSequential reads: no seeks, fastRandom read: about 16 µsRewrite in place: erase a whole blockBoth reward big sequential appends
HDDs punish jumping around; SSDs punish rewriting small pieces in place. Both reward big, sequential appends.

On the Kafka team's disk array, linear writes ran at "about 600MB/sec" and random writes at "about 100kB/sec". Appends beat scattered small writes, even on SSDs.

Seen this before?

The full latency table and the 6,000x gap are in Reads and writes.

03What write() and fsync() really do

write() copies your bytes into the page cache, marks those pages dirty and returns. The disk hasn't been touched. Kernel flusher threads write dirty pages back later, on rules in the kernel's vm settings such as dirty_expire_centisecs (how old dirty data may get) and dirty_ratio (how much dirty memory before a writer must flush itself). Pull the plug before then, and the data is gone.

That makes the page cache a write-back cache: it says "done" first and writes to the slower layer later. Kafka leans into this on purpose, letting the OS use "all free memory" as disk cache instead of caching inside the JVM.

fsync() is the courier's signature. Per the fsync(2) man page, it flushes the file's data and metadata to the device and blocks until the device reports it's done. But there's a trap in the same page: fsync "does not necessarily ensure" that the directory entry (the file's name) is on disk. A new or renamed file needs an fsync on its directory too.

Try this first

Your app writes config.json, calls fsync on it, and the machine crashes. On reboot the file is missing. What did you forget? (Answer: you never fsynced the directory, so the file's name was still only in memory.)

So the safe way to replace a file has four steps: write a temp file, fsync it, rename it over the old one, then fsync the directory. Here's the sequence.

Fig. 3

Saving a file safely takes two fsyncs, not one

Saving a file safely takes two fsyncs, not oneSequence between the app, the kernel page cache and the disk. 1: write the new contents to config.tmp. 2: fsync config.tmp, which pushes its data to disk. 3: rename config.tmp over config.json. 4: fsync the directory, which pushes the new name to disk. Only then is the save safe against a crash.1write(config.tmp)2fsync(config.tmp)file data reaches disk3rename(tmp, config.json)4fsync(directory)new name reaches diskOK: safe across a crashAppKernelpage cacheDiskSkip step 2 and a crash can leave an empty file. Skip step 4 and the name can vanish.
Four steps, and skipping either fsync leaves a window. A crash in that window loses the file's contents or its name.

Many candidates treat "write() returned" as durable

It only means the kernel has a copy in RAM. Durable means fsync returned, the directory is synced if the file is new, and the hardware didn't lie about its cache.

And even fsync has a dark corner. In 2018, Postgres learned that a failed background write could leave fsync returning success with the data already gone. That's fsyncgate, and it changed how serious databases handle errors.

Members-only article

Keep reading with the Article Pass

You've read the free part. The other 7 sections of this guide, with every diagram and worked example, are for members. One Article Pass unlocks every members-only article on Engineering Atlas: the ones live today and every one I publish while your pass is active.

  • Journaling filesystems: a write-ahead log inside the kernel
  • fsyncgate: when fsync said yes and meant no
  • Skipping the page cache: O_DIRECT
  • Waiting for I/O: epoll vs io_uring
  • How grep, ripgrep, find, locate and plocate search
  • Real systems: durability dials in the wild
  • How to say it in the HLD interview

From ₹49 · one-time payment, no auto-renewal · separate from course passes