Page cache and fsync: why "written" and "will not be lost" are different claims
`write` returns before the data reaches the disk: it reaches the kernel's cache. Everything else follows from that — why unflushed data survives a process crash but may be lost when the power goes, why a flush costs tens of times more than a write, and why databases commit transactions in batches.
Full technical treatment
TL;DR
write carries the data as far as the kernel's cache and returns — that is
where its job ends: "A successful return from write() does not make any
guarantee that data has been committed to disk". And that cache is the
kernel's memory, not the process's: the data in it is already shared, but
it is not durable yet. Durability is asked for separately, by a flush.
Hence the main consequence: the two failures are different. Measured: a
process wrote without flushing and was killed by SIGKILL — the data is there
and another process can read it, so a process crash loses nothing. When the
power goes, though, data that has not become durable yet may be lost, along
with the memory the cache lived in. Durability is priced accordingly: measured
on this machine, a 4 KiB write — 1.8 us, the same write with a flush —
164.7 us, a ratio of 90.1. Your numbers will differ; the
two-order-of-magnitude gap will not.
Beyond that is what separates knowing from having read. fsync and
fdatasync are indistinguishable here, and that is a result too: a gap of
0.9 us against a run-to-run spread of 51.1 us, and the block's last line,
difference resolvable on this machine, says no — a ratio smaller than the
noise may not be claimed. One flush per hundred records is far cheaper than a
hundred flushes: measured, 17.2 ms against 1.0 ms, which is exactly why
databases commit transactions in groups. fsync on a file does not make its
name durable: creating a file changes the directory, and a directory is
flushed separately — "For that an explicit fsync() on a file descriptor for the
directory is also needed". And closing a file flushes nothing: "Typically,
filesystems do not flush buffers when a file is closed" — the habit of "closed,
therefore saved" rests on nothing.
- a program writes data into a file and gets back either success or an error;
- a machine has RAM, which disappears when the power goes, and a storage device, which does not;
- a process can die while the machine keeps running — and the other way round.
- what the page cache is, what the kernel's writeback does, and how the kernel's cache differs from a buffer inside the program itself;
fsync,fdatasync,O_DIRECT, group commit, flushing a directory.
What is actually being asked
The ladder usually runs like this:
- "What does
writedo?" — the warm-up, where it becomes clear whether the candidate believes the data went to the disk. - "Is the data lost if the process crashes right after
write?" — where the substance begins. - "And if the machine goes down?" — the same question with a different answer, and the difference explains the whole mechanism.
- "How does
fsyncdiffer fromfdatasync?" — whether you know they differ over metadata. - "Why does a database not
fsyncon every write?" — a question about cost. - "Is
fsyncon the file enough for the file to exist?" — the question where a file's content and its name come apart.
The numbers come from running bench/durability/fsync.py and
bench/durability/practice.py. Timings depend heavily on the disk: yours will
differ. The observations do not depend on the machine: what survives a process
crash, and what each call promises.
Base: where the data is once the write returned success
Before talking about caches and flushes it is worth naming, in ordinary words, the path the data travels. It has four stretches:
- the application called write — it handed the kernel a chunk of memory and said which file to put it in;
- the kernel's memory — the data is accepted and sits in an area the kernel owns; the call returns at this point;
- the filesystem — it decides where on the device those bytes will land and which bookkeeping records have to change with them;
- the storage device — the place where bytes stay when there is no power.
The key question is which stretch the write call ends on. It ends on the second, not the fourth: the application gets success when the kernel took the data, not when the disk did. From there the kernel carries it onward — by itself, at its own pace — and the application neither waits for that nor hears about it.
And here is the question this lesson is about: the write returned success — where is the data really? Everything else follows from the answer: what happens to it if the process dies right now, what happens if the power goes, and what it costs to demand that the data already be on the device.
The short answer: the data is in the kernel's memory. That memory is shared — it does not belong to the process that wrote it and outlives that process's death. But it is also the memory that goes away with the power, so "the data was accepted" and "the data is durable" are two different statements, and the second one has to be asked for by a separate call.
That is already enough to answer the basic interview question. Everything below is about what that separate call costs, how its two forms differ, and why databases do not demand an acknowledgement per write.
Mechanism 1: write carries data to the cache, not to the disk
Start with what write(2) says about itself:
A successful return from write() does not make any guarantee that data has been
committed to disk. On some filesystems, including NFS, it does not even
guarantee that space has successfully been reserved for the data. In this case,
some errors might be delayed until a future write(2), fsync(2), or even
close(2).
Note the second half: an error may arrive later, and it may arrive in
close. That is the first practical consequence: code that ignores the result
of close loses write errors silently.
What write actually does is move the data into the page cache, the cache
of file pages living in the kernel's memory. From there the kernel's writeback
will put them on the disk at some point; when exactly, the application neither
knows nor decides.
And since the cache is the kernel's memory, "closed the file, therefore saved
it" rests on nothing either, which close(2) states separately:
A successful close does not guarantee that the data has been successfully saved
to disk, as the kernel uses the buffer cache to defer writes. Typically,
filesystems do not flush buffers when a file is closed.
Mechanism 2: a process crash and a power loss are different events
The most useful question in the topic: if the data is not on the disk yet, what
loses it? Check it the crudest way available. A process writes to a file, never
calls fsync — and receives SIGKILL, dying without a single line of cleanup:
2. WHOSE MEMORY THE PAGE CACHE IS
---------------------------------
child wrote without fsync, then SIGKILL exit -9
file content after the kill survived the kill
exit -9 is how subprocess reports a death by signal: the negative number is
the signal number itself. A shell in the same place would print 137, that is
128 + 9 (the lesson on signals).
The data is there. What is more, another process sees it: bench/durability/practice.py
prints the file twice — once read back after the writer died, once from an
unrelated process — and both lines are the same, because reads go through the
same cache.
Hence the distinction worth answering with:
- A process crash loses nothing that made it into
write. The cache outlives a crash, a kill and the OOM killer alike. - A power loss is another matter: data that has not become durable yet may be lost there, because the memory the cache lived in goes off with the machine.
Note the "may be". There is no guarantee that unflushed data survives; there is no guarantee that it is certainly gone either — the kernel's writeback may have carried some of those pages to the device already, and the application has no idea which ones. Nothing can be planned on that: from the application's point of view, everything unflushed is data whose fate after a power cut is simply unknown.
That is why "we are fine, the application just restarted" is a legitimate sentence and "we are fine, the machine just rebooted" is not.
Mechanism 3: what fsync promises and what it costs
The promise is written in fsync(2):
fsync() transfers ("flushes") all modified in-core data of (i.e., modified
buffer cache pages for) the file referred to by the file descriptor fd to the
disk device.
The price of that promise is the lesson's main measurement:
1. THE PRICE OF DURABILITY, PER WRITE
-------------------------------------
write only (4 KiB), median 1.4 us
write + fdatasync, median 142.9 us
write + fsync, median 143.9 us
fsync over plain write 104x
gap between fsync and fdatasync 0.9 us
run-to-run spread of the same mode 51.1 us
difference resolvable on this machine no
This block comes from the bench/durability/fsync.py run. The exercises below
quote a second run, bench/durability/practice.py, where the same pair came out
at 1.8 and 164.7 us and the
ratio at 90.1. Two runs on one machine diverge exactly as the run-to-run spread line promises: what carries meaning is the order of the ratio, not its
digits.
Two things in that block matter in different ways.
The ratio carries meaning. A hundredfold: that is what separates "the data is in kernel memory" from "the data is on the device". It is not a Linux quirk or an oversight but physics: the second case has to wait for a real device to acknowledge the write.
The gap between fsync and fdatasync is not resolvable here, and the script
says so itself. A gap of 0.9 us against a spread of 51.1 us between repeats of
the same mode — the script measures each mode five times over, which is the
run-to-run spread of the same mode line. No ratio may be claimed from that,
and the block's last line, difference resolvable on this machine, says no. The
difference between the calls is real and documented:
fdatasync() is similar to fsync(), but does not flush modified metadata unless
that metadata is needed in order to allow a subsequent data retrieval to be
correctly handled.
So fdatasync saves the metadata write — and the saving shows up where metadata
is expensive: when a file grows, on filesystems that journal metadata, on slow
devices. On this container's virtual disk it drowned in the spread, and the
honest answer here is "not measured", not "the same".
Mechanism 4: why a database does not fsync on every write
The ratio from the previous block is the price of one promise. What follows is arithmetic: how many times to demand it.
3. ONE FSYNC FOR MANY RECORDS, OR ONE EACH
------------------------------------------
100 records, fsync after each 17.2 ms
100 records, one fsync at the end 1.0 ms
ratio 17x
Same bytes, same disk, and after the last line the data is equally durable in both cases. What differs is the number of points at which acknowledgement was demanded.
Hence group commit: transactions arriving at almost the same time wait for a shared flush. Each waits longer than it would alone, but one latency serves the whole group rather than each transaction separately.
And hence the answer to "why not just fsync every request": you can, and it
will be correct. The cost is that ratio multiplied by the number of requests,
and it turns into a ceiling on throughput.
Deeper: a file's name does not live in the file
The last step is about the place where the durability of the content and of the
name come apart. You created a file, wrote to it, called fsync, and the
machine went down. Is the file guaranteed
to exist?
fsync(2) answers without ambiguity:
Calling fsync() does not necessarily ensure that the entry in the directory
containing the file has also reached disk. For that an explicit fsync() on a
file descriptor for the directory is also needed.
4. WHAT FSYNC IS FOR: THE FILE, NOT THE DIRECTORY
-------------------------------------------------
file synced yes
directory sync cost 0.11 ms
The measurement here does not answer whether the directory needs its own flush — the man page does — but what it costs.
A file's content and the fact that it exists under that name are two different objects, flushed separately. Hence the sequence for an atomic file replacement, which is what gets asked: write to a temporary file, flush it, rename it over the target, flush the directory. Skipping the last step gives you code that works right up to the first power cut, and then loses precisely what it thought it had written.
How to answer in an interview
Short answer: write carries data only as far as the kernel's cache and
returns; durability is promised by fsync, and it costs two orders of magnitude
more. That is why a process crash loses nothing written — the cache belongs to
the kernel — while a power loss may lose everything unflushed.
That is enough for a correct answer. What follows is what you add when the interviewer digs.
If the interviewer digs deeper
Three things separate a good answer. First, you distinguish the two failures, a
dying process and a power cut, and say why the answers differ. Second, you give
the cost as a number and know what follows from it: group commit exists not for
elegance but because flushing on every write runs into the device. Third, you
remember the directory: fsync on a file makes the content durable, not the
name, and an atomic replacement has to flush both.
And one piece of care in phrasing that gets noticed. "A machine crash loses the data" is too strong a claim; the correct form is that data which has not become durable yet may be lost when the power goes. The kernel's writeback may have carried part of it to the device already, and which part is unknown to the application. The practical conclusion does not change, but the statement becomes true: unflushed data cannot be counted on in either direction.
Next they ask
If fsync is that expensive, can one simply rely on the cache?
You can — right up to the first power cut. Everything unflushed lives in the kernel's memory, so in that sense "the data is written" means "the data is written for as long as the machine stays up".
So the practical choice is not "flush or not" but where to put the line:
which data must survive a power cut and which may be lost. A transaction
log must; a cache of rendered images need not. That line is what decides where
fsync belongs in the code.
Will a write error arrive in write?
Not necessarily. write(2) warns outright that some errors are deferred to a
later write, to fsync, or even to close. The reason is the same: during
write nothing reached the device, so nothing is known about the device yet.
Hence the rule most often broken in code: the result of close must be
checked, as must the result of fsync. Code that closes a file without
checking loses precisely the errors the whole exercise was about.
Does fsync help if the disk lies about completing a write?
No, and that is the boundary of the promise. fsync pushes data to the device
and waits for its acknowledgement; if the device acknowledges before the data is
in non-volatile memory, the guarantee breaks at a level the kernel cannot reach.
Practically this means durability is a property of the whole chain: application, kernel, filesystem, controller, disk. It is verified not by reasoning but by a sudden power-loss test — which is why such tests are part of accepting a storage system.
What does O_DIRECT give you?
Not durability but a bypass of the cache: data goes past the page cache and the application takes over buffering and alignment. Durability does not follow — the device may still hold the write in its own cache, which is why databases combine direct I/O with flushing.
The point of O_DIRECT is different: when an application already keeps its own
cache (and a database does), the kernel's cache duplicates it and spends the
memory twice.
Common misconceptions
if write returned successfully, the data is on the disk
It is not: "A successful return from write() does not make any guarantee that data has been committed to disk". The data reached the kernel's cache, and when it reaches the device is not the application's decision. Only fsync promises that — and it costs two orders of magnitude more: measured 1.8 us against 164.7.
closing the file means saving it
close(2) answers directly: "Typically, filesystems do not flush buffers when a file is closed". What is more, an error from an earlier write may surface in close itself — which is why its result must be checked.
if the process crashes before fsync, the data is lost
It is not. Measured: a process wrote without flushing and was killed by SIGKILL — the content is there and visible to another process. The cache belongs to the kernel, not to the process. When the power goes, though, unflushed data may be lost along with the memory the cache lived in — and that is the whole difference between the two failures.
fdatasync is noticeably faster than fsync
On this machine the difference is not resolvable: a gap of 0.9 us against a run-to-run spread of 51.1 us, and the script prints no in its difference resolvable on this machine line. The difference between the calls is real and documented — fdatasync skips metadata that reads do not need — but the saving appears where metadata is expensive, not everywhere.
fsync on every write is correct, just slow
It is correct and it costs a measurable amount: a hundred records with a flush after each — 17.2 ms, the same hundred with one flush at the end — 1.0 ms, seventeen times apart. After the last line the durability is identical. What differs is the number of acknowledgements demanded, and group commit in databases is built on exactly that.
fsync on a file guarantees the file exists
It guarantees its content is durable. The name lives in the directory, which is a separate object: "For that an explicit fsync() on a file descriptor for the directory is also needed". Hence the mandatory last step of an atomic replacement — flushing the directory.
O_DIRECT makes a write durable
O_DIRECT bypasses the kernel's cache but not the device's: the acknowledgement may come before the data is in non-volatile memory. That is why databases combine direct I/O with flushing rather than substituting one for the other.
Practice
Two exercises. Answer first, then check against the real output: in both, the correct answer comes from a script's committed output rather than being written by hand.
Practice · predict the output
print(content_after_kill(path) or "(empty)") print(seen_by_another_process(path) or "(empty)")
Practice · estimate
Knowledge check
A process wrote data to a file and was killed by SIGKILL before calling fsync. What happened to the data?
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
writecarries the data as far as the kernel's cache and returns — that is where its job ends: "A successful return from write() does not make any guarantee that data has been committed to disk". And that cache is the kernel's memory, not the process's: the data in it is already shared, but it is not durable yet. Durability is asked for separately, by a flush.- Hence the main consequence: the two failures are different. Measured: a process wrote without flushing and was killed by
SIGKILL— the data is there and another process can read it, so a process crash loses nothing. When the power goes, though, data that has not become durable yet may be lost, along with the memory the cache lived in. Durability is priced accordingly: measured on this machine, a 4 KiB write — 1.8 us, the same write with a flush — 164.7 us, a ratio of 90.1. Your numbers will differ; the two-order-of-magnitude gap will not. - Beyond that is what separates knowing from having read.
fsyncandfdatasyncare indistinguishable here, and that is a result too: a gap of 0.9 us against a run-to-run spread of 51.1 us, and the block's last line,difference resolvable on this machine, saysno— a ratio smaller than the noise may not be claimed. One flush per hundred records is far cheaper than a hundred flushes: measured, 17.2 ms against 1.0 ms, which is exactly why databases commit transactions in groups.fsyncon a file does not make its name durable: creating a file changes the directory, and a directory is flushed separately — "For that an explicit fsync() on a file descriptor for the directory is also needed". And closing a file flushes nothing: "Typically, filesystems do not flush buffers when a file is closed" — the habit of "closed, therefore saved" rests on nothing.
In fact
- It is not: "A successful return from write() does not make any guarantee that data has been committed to disk". The data reached the kernel's cache, and when it reaches the device is not the application's decision. Only
fsyncpromises that — and it costs two orders of magnitude more: measured 1.8 us against 164.7. close(2)answers directly: "Typically, filesystems do not flush buffers when a file is closed". What is more, an error from an earlier write may surface incloseitself — which is why its result must be checked.- It is not. Measured: a process wrote without flushing and was killed by
SIGKILL— the content is there and visible to another process. The cache belongs to the kernel, not to the process. When the power goes, though, unflushed data may be lost along with the memory the cache lived in — and that is the whole difference between the two failures. - On this machine the difference is not resolvable: a gap of 0.9 us against a run-to-run spread of 51.1 us, and the script prints
noin itsdifference resolvable on this machineline. The difference between the calls is real and documented —fdatasyncskips metadata that reads do not need — but the saving appears where metadata is expensive, not everywhere. - It is correct and it costs a measurable amount: a hundred records with a flush after each — 17.2 ms, the same hundred with one flush at the end — 1.0 ms, seventeen times apart. After the last line the durability is identical. What differs is the number of acknowledgements demanded, and group commit in databases is built on exactly that.
- It guarantees its content is durable. The name lives in the directory, which is a separate object: "For that an explicit fsync() on a file descriptor for the directory is also needed". Hence the mandatory last step of an atomic replacement — flushing the directory.
- O_DIRECT bypasses the kernel's cache but not the device's: the acknowledgement may come before the data is in non-volatile memory. That is why databases combine direct I/O with flushing rather than substituting one for the other.
What is covered
- What is actually being asked
- Base: where the data is once the write returned success
- Mechanism 1: write carries data to the cache, not to the disk
- Mechanism 2: a process crash and a power loss are different events
- Mechanism 3: what fsync promises and what it costs
- Mechanism 4: why a database does not fsync on every write
- Deeper: a file's name does not live in the file
- How to answer in an interview
- Next they ask
- Common misconceptions
- Practice
- Knowledge check
Sources & further reading
3 SOURCES
- fsync(2), Linux man-pages 6.7Official documentation. What the flush promises: "fsync() transfers ('flushes') all modified in-core data of (i.e., modified buffer cache pages for) the file referred to by the file descriptor fd to the disk device". And what it does NOT promise, which is half the lesson: "Calling fsync() does not necessarily ensure that the entry in the directory containing the file has also reached disk. For that an explicit fsync() on a file descriptor for the directory is also needed". The difference from fdatasync: "fdatasync() is similar to fsync(), but does not flush modified metadata unless that metadata is needed in order to allow a subsequent data retrieval to be correctly handled".https://man7.org/linux/man-pages/man2/fsync.2.html
- write(2), Linux man-pages 6.7Official documentation. Why a return from write says nothing about the disk: "A successful return from write() does not make any guarantee that data has been committed to disk. On some filesystems, including NFS, it does not even guarantee that space has successfully been reserved for the data. In this case, some errors might be delayed until a future write(2), fsync(2), or even close(2)".https://man7.org/linux/man-pages/man2/write.2.html
- close(2), Linux man-pages 6.7Official documentation. Against the habit of "closed, therefore saved": "A successful close does not guarantee that the data has been successfully saved to disk, as the kernel uses the buffer cache to defer writes. Typically, filesystems do not flush buffers when a file is closed".https://man7.org/linux/man-pages/man2/close.2.html