GitHub Open console
← All writing
Aug 31, 2026 · engineering

flush() is not fsync()

A durability bug that every test passed. The log was appending correctly and replaying correctly, and it would still have lost your data on power loss.

The write path had a line in it that looked finished:

wal.rs — before
self.writer.flush()?;

It is a BufWriter. flush() empties the userspace buffer and hands the bytes to the operating system with a write syscall. After it returns, the bytes have left the process. Kill the process with SIGKILL at that moment and they survive, because the kernel already has them.

So the crash test passed. And the durability claim was still false.

Where the bytes actually are

The kernel does not hand a write straight to the disk. It puts it in the page cache and returns immediately, then writes it out whenever it feels like — which can be seconds later. For those seconds the only copy of your acknowledged write is in volatile memory.

A process crash does not touch the page cache, so the data survives. A power cut, a kernel panic, or someone pulling the plug does, and the write is gone — after the database told the client it was safe. That is the exact promise a write-ahead log exists to make, broken.

wal.rs — after
// flush() only hands the bytes to the OS, which may hold them in its page
// cache for seconds — that survives a process crash but NOT power loss.
// sync_all() forces the disk to actually store them, which is the whole
// point of a write ahead log. It is also the slowest line in the database.
fn sync(&mut self) -> io::Result<()> {
    self.writer.flush()?;
    self.writer.get_ref().sync_all()
}

sync_all() is fsync. It blocks until the drive confirms the data is on durable media. Both put and delete call it before returning, so by the time a write is acknowledged it is genuinely on disk.

It is also the slowest line in the database

That comment is not a joke. An fsync is orders of magnitude slower than a buffered write, and now there is one on every single write. Every high-throughput trick that comes later — batching several writers into one fsync, amortising the cost across concurrent commits — exists because of this line.

The lesson I actually took: a test that kills the process is not a durability test. It is a process-crash test. They are different failures, and the cheap one passes for the wrong reason.