Understanding the Linux Page Cache and How Writeback Actually Works
Why 'free' memory on a Linux system is mostly a lie in the useful direction, and how dirty pages actually get from RAM to disk without every write() blocking on it.
The single most common Linux memory-management misconception is treating free -h’s “free” column as the true measure of available memory. On any Linux system that’s been running for more than a few minutes, most of what looks like “used” memory is actually page cache — file data the kernel has opportunistically kept in RAM because reading from RAM is dramatically faster than reading from disk again, and that cache is reclaimed instantly the moment an application actually needs the memory for something else. Understanding this, and understanding how writes interact with that same cache, explains a lot of Linux I/O behavior that otherwise looks mysterious.
Reads: caching is the easy direction
When a process reads a file, Linux doesn’t just hand back the requested bytes and forget about them — it keeps the underlying pages in the page cache, keyed to the specific file and offset they came from. A subsequent read of the same data, by the same process or a completely different one, finds the pages already resident in RAM and skips the disk entirely. This is why the second time you read a large file is often dramatically faster than the first, and why a freshly booted system with an empty cache behaves noticeably slower than the same system after it’s been running for a while and the cache has filled with frequently-accessed data.
Writes: the harder direction, and why it matters more
Writes are where the interesting complexity lives. When a process calls write() on a regular file (without O_DIRECT or O_SYNC), the data isn’t synchronously written to disk before the syscall returns — it’s copied into the page cache, the affected pages are marked dirty (meaning: cache content that differs from what’s actually on disk), and write() returns immediately. The application sees a fast write; the actual disk I/O to make that data durable happens later, asynchronously, via a mechanism called writeback.
This is a deliberate, load-bearing design choice, not a shortcut. It means applications doing many small writes aren’t individually blocked on disk latency for each one — the kernel batches, reorders, and schedules the actual disk writes far more efficiently than issuing one disk write per application-level write() call ever could. The tradeoff, unavoidably, is that data isn’t actually durable the instant write() returns; a power loss or kernel crash between the write and the writeback that flushes it to disk means that data is lost, which is exactly why applications that need guaranteed durability at a specific point (a database committing a transaction, for instance) explicitly call fsync() rather than trusting an ordinary write() to have reached disk.
The writeback mechanism itself
Dirty pages don’t sit in memory indefinitely — the kernel’s writeback machinery (historically pdflush, now handled by per-backing-device flusher threads, bdi-* kernel threads) wakes periodically and flushes dirty pages to their backing storage, governed by a set of tunables under /proc/sys/vm/:
cat /proc/sys/vm/dirty_ratio
cat /proc/sys/vm/dirty_background_ratio
dirty_background_ratio sets the percentage of total system memory that can become dirty before the kernel proactively starts background writeback, asynchronously, without blocking any application. dirty_ratio sets a higher threshold at which the kernel switches to a much more aggressive mode: if dirty pages accumulate faster than writeback can flush them and cross this threshold, the kernel starts throttling — actually blocking processes attempting further writes until enough dirty data has been flushed to bring the ratio back down. This is the mechanism that prevents a write-heavy workload from accumulating unbounded gigabytes of unflushed dirty data that a crash could lose entirely and that would take an impractically long time to flush all at once if allowed to grow without limit.
Why this explains “my disk is full but df says there’s space” and similar oddities
Understanding writeback explains several behaviors that otherwise look like bugs. A sync or unmount that suddenly takes a long time on a system that’s been doing heavy writes isn’t hanging arbitrarily — it’s forcing all outstanding dirty pages to actually flush to disk before completing, which takes real time proportional to how much dirty data had accumulated. A system whose write throughput suddenly drops under heavy sustained write load isn’t necessarily hitting a hardware limit — it may have crossed dirty_ratio and entered write-throttling, deliberately slowing application writes to let the disk catch up.
Checking actual dirty page state
/proc/meminfo reports the current amount of dirty (unflushed) and writeback-in-progress memory directly:
grep -E '^(Dirty|Writeback):' /proc/meminfo
A Dirty value that stays persistently large and doesn’t drain over time, even under light ongoing write load, can indicate a writeback problem — a slow or failing storage device that can’t keep up with even modest write rates, or a misconfigured dirty_expire_centisecs setting causing dirty data to linger far longer than intended before the kernel prioritizes flushing it.
The genuine tradeoff this whole system represents
Page cache and writeback together represent a deliberate bet: that decoupling application-visible write latency from actual disk write latency, and giving the kernel freedom to batch and reorder writes for efficiency, is worth the durability gap between “the application believes this is written” and “this is actually on persistent storage.” For the overwhelming majority of workloads this bet pays off enormously in throughput and responsiveness. For the specific subset of operations where durability at an exact moment genuinely matters — a database transaction commit, a critical configuration change, anything where “the write() call returned” needs to actually mean “this survived a crash right now” — fsync() (or O_DIRECT/O_SYNC at the file-descriptor level) exists precisely to opt back into synchronous durability for that specific write, without giving up the performance benefit of asynchronous writeback for everything else.