Articles

What Is fsync? How Writes Actually Reach the Disk

fsync forces data in the OS page cache out to durable storage. Why a successful write() is not a durable write, and how databases use fsync safely.

The Lycoris Team The Lycoris Team · · 5 min read
Server racks in a data center aisle

fsync is the system call that tells the operating system to push a file’s buffered data all the way to durable storage and not return until it is there. It exists because a successful write() does not mean your data is safe. On most systems, write() only copies bytes into an in-memory cache, and the kernel flushes them to disk later on its own schedule. If the machine loses power in between, those writes are gone. fsync closes that gap, and it is the foundation every durable database is built on.

Why write() is not enough

When an application calls write() on a file, the kernel copies the data into the page cache, a region of RAM that holds recently read and written file contents. The call returns almost immediately. The data is now visible to every other process reading that file, so from the application’s point of view the write looks complete.

But nothing has touched the disk yet. The kernel marks those pages as dirty and writes them back in the background, typically after a delay or when memory pressure builds. This design is a huge performance win: it lets the kernel batch many small writes into fewer large ones, and it means repeated writes to the same region may only hit the disk once.

The cost is a durability window. A crash or power loss before writeback means the data that every process could see a moment ago simply never existed on disk. For a log file, that might be acceptable. For a bank ledger, it is not.

What fsync does

Calling fsync(fd) asks the kernel to:

  1. Write all dirty pages for that file from the page cache to the storage device.
  2. Write the file’s metadata, such as its size and modification time, so the file can be read back correctly.
  3. Ask the device to flush its own volatile write cache, so the data is on persistent media rather than sitting in the drive’s internal buffer.
  4. Return only once all of that is acknowledged.

Step 3 matters more than it looks. Storage devices have their own caches, and without an explicit flush command a drive can report a write as finished while the bytes are still in volatile memory on the controller. Drives with power-loss protection, common in enterprise SSDs, have capacitors that let them finish those writes during an outage, which makes flushes much cheaper.

fsync, fdatasync, and friends

Linux and other Unix-like systems offer several related calls:

CallWhat it flushesTypical use
fsync(fd)File data and all metadataGeneral-purpose durability
fdatasync(fd)File data, plus only the metadata needed to read it backAppend-heavy logs where timestamps do not matter
sync()Everything dirty on every file systemShutdown scripts, rarely in applications
O_SYNC / O_DSYNC flagsMake every write behave as if followed by fsync or fdatasyncSimple code paths that want durability per write

fdatasync can be noticeably faster because it skips flushing metadata like access times when the file size has not changed. Databases that pre-allocate their log files take advantage of this.

The directory gotcha

Creating or renaming a file changes the directory, not just the file. If you create a new file, write to it, and fsync it, the contents are durable, but the directory entry pointing to it might not be. After a crash, the file could be missing entirely.

The fix is to also open the parent directory and fsync it. The classic safe pattern for atomically replacing a file is:

  1. Write the new contents to a temporary file in the same directory.
  2. fsync the temporary file.
  3. rename() it over the original. Rename is atomic on POSIX file systems.
  4. fsync the directory so the rename itself is durable.

Skip any of those steps and you can end up with an empty file, a half-written file, or the old version after a crash.

How databases use fsync

Databases need durability without paying the cost of an fsync on every page they modify. The standard answer is the write-ahead log. Before changing any data pages, the database appends a description of the change to a sequential log file and fsyncs that log. Once the fsync returns, the transaction can be reported as committed, satisfying the “D” in ACID. The data pages themselves can be written back lazily, because the log can replay them after a crash.

Two techniques make this affordable:

  • Group commit — instead of one fsync per transaction, the database waits a fraction of a millisecond, gathers every transaction that committed in that window, and covers all of them with a single fsync.
  • Sequential appends — writing to the end of a log is far friendlier to storage than scattered writes across data files, so each fsync has less work to do.

Distributed systems layer on top of this. In consensus protocols like Raft, a node must fsync a log entry before acknowledging it to the leader, otherwise a crash could make the cluster forget an entry it believed was safely replicated.

When fsync fails

fsync can return an error, usually because the storage device reported a write failure. The tricky part is what happens next. On Linux, once writeback fails, the kernel may mark the dirty pages clean and discard them, and a subsequent fsync can return success even though the data was never written. Retrying fsync is therefore not a safe recovery strategy.

Several databases changed their behavior after this was widely discussed: on an fsync error they now stop and recover from their write-ahead log rather than retry. If you write your own storage code, treat an fsync failure as “the state of this file is unknown” and recover from a known-good source.

Performance and the temptation to skip it

fsync is slow relative to an in-memory write because it waits on the device. On spinning disks, each fsync can cost milliseconds. On NVMe drives it is much faster, but still orders of magnitude slower than a cached write.

That cost leads many systems to offer a knob that relaxes it: fsync every second instead of every commit, or leave flushing entirely to the operating system. These settings are legitimate for caches, analytics staging tables, or replicas that can be rebuilt. Just make the choice consciously, because it converts “the database said committed” into “the database probably committed.”

The takeaway

A write() call lands your data in the operating system’s page cache, not on disk. fsync forces it, along with the metadata needed to find it, through the page cache and the drive’s own buffer onto persistent storage. Databases make this affordable with write-ahead logs and group commit. If you handle files that must survive a crash, fsync the file, fsync its directory after creating or renaming, and treat an fsync error as data loss rather than something to retry.

Chisato Chisato · · 4 min read

ACID vs BASE: Two Approaches to Database Consistency

ACID guarantees strict consistency after every transaction; BASE trades that for availability and scale. How each model works and when to pick one.

#Databases #Computer Science #Backend
The Lycoris Team The Lycoris Team · · 4 min read

What Is MVCC? Multi-Version Concurrency Control

MVCC lets readers and writers work on a database concurrently without blocking each other, by keeping multiple versions of each row instead of locking it.

#Databases #Computer Science #Backend