Reads and writes look symmetric in the API: read(fd, buf, count) and write(fd, buf, count). They are not symmetric in behavior, and the difference is buffering.
When you read, the layers fill their buffers before returning. If something is wrong — the file is shorter than you asked for, the disk errors, the network filesystem is down — the failure surfaces before the read returns. You find out while you're still in a position to do something about it.
When you write, the layers accept your bytes and return success. The actual write happens later: when the buffer fills, when you flush, when you seek, when you close. Error reporting is delayed by exactly the same amount. If you don't check the return codes of fwrite, fflush, fseek, and fclose, you are not checking whether your writes succeeded at all.
That's Fabian Giesen's point, and it's one of those things that is obvious once stated and easy to ignore in practice.
The layers
Even if you do check write, you've only reached the kernel's buffer cache. The data hasn't necessarily hit the device. The kernel will write it back eventually; if the machine crashes first, you lose it. That's usually fine and occasionally catastrophic.
Then there are the device and RAID controller caches. They're supposed to have capacitors so they can finish writing on power loss. Hopefully that's true. Good luck.
fsync is the syscall that asks for durability, and it's the point where the API stops pretending. But there's a final wrinkle: fsync on a file does not guarantee the directory entry pointing to that file has reached disk. For that you need to fsync the directory too. The Linux man page says so explicitly, and almost nobody does it.
The comment thread on the post is worth reading for the safety dance that databases actually use: preallocate, fsync the temp file, fsync the directory, rename, fsync the file, fsync the directory. SQLite does a version of this. Most application code does none of it.
Why this is a structural problem, not a bug
The asymmetry isn't an implementation mistake in libc. It's what buffering means. A buffer exists to avoid a syscall per byte; to do that, it must report success before the syscall happens. Any layer that acknowledges early has this property, and we have many of them: page cache, device cache, network filesystems, cloud storage SDKs that return before replication.
So the right mental model is a ladder of acknowledgements, each one weaker than it sounds:
- The bytes are in your process's buffer.
- The bytes are in the kernel's page cache.
- The bytes are on the device, as far as the device admits.
- The bytes are on stable media.
- The directory entry that names them is also on stable media.
Only the last two are what most people mean by "saved," and the API gives you (1) for free and charges you real time for (4) and (5).
What to do about it
Not "always fsync" — that would make every write slow, and most writes don't need it. The useful discipline is to decide, per write path, which rung you need, and to encode that in the function rather than in a comment.
A configuration file that you write once at shutdown probably wants (5). A log line can live with (1). A database commit wants (4) plus whatever the write-ahead log needs. The mistake is assuming (1) means (4) because the call returned zero.
It also helps to know that close can report errors, especially on network filesystems. Code that writes, closes, and never checks the close is code that can silently lose the last buffer.
The generalization
"Success reported too early" is a smell that shows up far beyond files. A message queue that acknowledges on enqueue, a database that acknowledges before replication, a cloud SDK that returns after the local socket write — all of them have a version of this ladder, and all of them will tell you a comforting lie if you don't ask which rung you're on.
The fix is the same in each case: know your durability requirement, use the API that provides it, and don't confuse a returned handle with a completed write.
Giesen's post is from 2015 and it hasn't aged, because the layers haven't gone anywhere. The dragons are still there; they're just quieter.
Source: Reading and writing are less symmetric than you (probably) think — Fabian Giesen.