The problem: disk is slow and RAM is a bounded copy#
TL;DRthe 30-second version
- A file lives on disk, which is slow. The page cache keeps recently-used pieces of files (called pages, usually 4 KB each) in a pool of RAM so most reads and writes hit memory instead of disk.
- A read looks for its page in the cache. If it is there, that is a hit and no disk access happens. If not, that is a miss, and the kernel faults the page in from disk into a free frame β a slot of RAM that holds one page.
- A write updates the page in RAM and marks it dirty, meaning RAM now holds newer data than disk. The write returns right away; the disk update is deferred. A writeback later flushes dirty pages to disk and marks them clean again.
- RAM is bounded, so when the cache is full the kernel must evict a page to make room. Here is the twist a plain cache never shows: a clean page can be dropped for free because disk already has an identical copy, but a dirty page must be written back to disk first, or the write would be lost.
- So the page cache is a bounded, write-deferring copy of your files. The clean-versus-dirty distinction is what makes it more than a lookup table: it decides which evictions are free and which stall on a disk write.
Reading a byte from RAM takes about 100 nanoseconds. Reading it from a solid-state disk takes tens of microseconds, and from a spinning disk, milliseconds. That is a gap of a hundred to ten thousand times. If every file access paid the disk price, no program would feel fast. The operating system closes that gap with the page cache: a slice of otherwise-free RAM where the kernel keeps copies of file pages it has touched recently, betting that they will be touched again soon.
Files are handled in fixed-size chunks called pages, almost always 4 KB. The unit of RAM that holds one cached page is called a frame. When you read one byte, the kernel does not fetch one byte; it brings the whole 4 KB page into a frame and serves your byte from there. The next read of any byte on that page is then free, because the page is already resident in RAM.
The reason this is harder than it looks is that RAM is small and files are large. The cache can only hold so many frames, so it constantly makes room by throwing pages out. And because writes are allowed to sit in RAM before reaching disk, throwing a page out is not always free: if the page was written to and disk has not caught up, the kernel owes the disk that update before the frame can be reused. Everything below is about that bargain.
How it works: fault in, mark dirty, write back, evict#
There are only three things you can do to the page cache, and one thing it does to itself. You read a page, you write a page, and you flush dirty pages back to disk. Under the covers, when RAM fills up, it evicts pages to make room. Let us take them one at a time.
A read first checks whether the page is already resident in a frame. If it is, that is a cache hit: the kernel copies the bytes out of RAM and you are done, with no disk access at all. If the page is not resident, that is a cache miss, which the kernel handles as a page fault: it finds a free frame, reads the 4 KB page from disk into it, and then serves your bytes. The page is now clean, meaning its RAM copy is byte-for-byte identical to disk. The next read of that page will be a hit.
A write also targets a page. If the page is resident, the write updates the bytes in RAM and marks the frame dirty: RAM now holds newer data than disk. Crucially, the write returns immediately, without waiting for the disk. If the page is not resident, the kernel must fault it in first (you cannot modify a page that is not in memory), and then apply the write and mark it dirty. Either way, disk is now stale for that page until a writeback catches it up.
A writeback is the reconciliation step. It walks the dirty pages, writes each one's contents down to disk, and marks it clean again. The page stays in the cache β it is likely to be read again β but disk has now caught up. In a real kernel this happens continuously in the background on a timer, and on demand when you call fsync to force your data to be durable. The key effect is batching: if you wrote the same page a thousand times, a single writeback settles all thousand changes with one disk write.
Now the part a plain cache never has to think about: eviction under memory pressure. Memory pressure just means the cache is full and something new needs a frame. The kernel picks a victim, roughly the least-recently-used page. If that victim is clean, reclaiming it is free: disk already holds the same bytes, so the kernel drops the frame and moves on. If the victim is dirty, the kernel cannot just drop it, or the write it holds would vanish. It must write the page back to disk first, then free the frame. That forced writeback is the hidden cost of a dirty page, and it is why the kernel prefers to reclaim clean pages when it can.
- Read a page: if resident, serve from RAM (a hit); otherwise fault it in from disk (a miss), then serve. The page ends up clean.
- Write a page: fault it in first if needed, update RAM, mark it dirty. Disk is now behind. The write returns without touching disk.
- Writeback: flush each dirty page to disk and mark it clean. The page stays cached.
- Evict (on memory pressure): pick the least-recently-used page. If clean, drop it for free. If dirty, write it back first, then free the frame.
A walkthrough: why the same eviction can be free or expensive#
Picture a cache with room for two frames and a file of many pages. You read page 0 and page 1. Both miss, so both are faulted in from disk, and both are clean. The cache is now full, and both copies match disk exactly.
Now you read page 2. The cache is full, so a frame must be reclaimed. The least-recently-used page is page 0, and it is clean, so the kernel drops it for free and faults page 2 in. No writeback happened, because disk already had page 0. This is the cheap path: eviction is a single disk read for the incoming page and nothing else.
Rewind and change one thing. Instead of reading pages 0 and 1, you write to them. Now both frames are dirty: RAM holds edits that disk has never seen. You read page 2. The cache is full again, but this time every resident page is dirty. The kernel cannot drop page 0 for free, because that would throw away your edit. It must write page 0 back to disk first, then free its frame, then fault page 2 in. The same eviction now costs an extra disk write. That is the price of a dirty page, and it is exactly the moment the dirty flag was invented to handle.
PredictYou write to a page, then immediately read it back before any writeback runs. Does the read hit or miss, and what does it return β the old disk value or your new one?
Hint: A read only touches disk on a miss. The written page is still resident.
It is a hit, and it returns your new value. The write updated the page in RAM and marked it dirty; the read finds that same resident frame and serves the RAM copy, which is the newest. Disk still holds the old value, but nobody reads disk on a hit. That is precisely why a program can write and re-read data far faster than the disk could sustain, and also why an un-flushed write is lost if the machine crashes first.
What it costs#
The page cache's whole value is turning slow disk accesses into fast RAM accesses, so the numbers that matter are how often you hit and what a miss costs. A hit is a memory copy, on the order of 100 nanoseconds. A miss is a disk read, tens of microseconds on a solid-state drive or milliseconds on a spinning one, plus the memory copy. A well-behaved workload whose working set (the pages it actively uses) fits in RAM runs almost entirely on hits.
| Operation | Disk I/O | Rough cost |
|---|---|---|
| Read hit | none | ~100 ns (RAM copy) |
| Read miss | 1 read | ~10β100 Β΅s + copy |
| Write to a resident page | none (deferred) | ~100 ns, marks dirty |
| Writeback / fsync of a dirty page | 1 write | ~10β100 Β΅s per page |
| Evict a clean page | none | drop the frame, free |
| Evict a dirty page | 1 write | forced writeback first |
Two effects make the average cost far lower than the miss cost suggests. First, spatial locality: because the kernel caches a whole 4 KB page, reading one byte makes the next several thousand bytes free. The kernel leans into this with read-ahead, fetching pages just past the one you asked for on the guess that you are reading sequentially. Second, write batching: many writes to the same dirty page collapse into a single writeback, so a burst of updates costs one disk write instead of one per update. Deferring the write is what lets it be batched.
The trade-offs
Deferring writes is the central bargain, and like every cache it trades speed for a window of risk. A write that has landed in RAM but not yet been written back is fast, but it is not durable: if the machine loses power in that window, the data is gone, because the only copy was in volatile memory. The kernel bounds this window with a timer (dirty pages older than a few seconds get flushed) and a ceiling on how much of RAM may be dirty at once. A program that needs a write to survive a crash must call fsync, which forces a writeback and waits for the disk to confirm.
- Speed versus durability: writes return the instant they hit RAM, but they are not safe until a writeback reaches disk. fsync trades the speed back for a durability guarantee.
- Batching versus latency: letting dirty pages accumulate lets many writes settle in one flush, but too many dirty pages means a longer, spikier flush and more data at risk. Kernels cap the dirty ratio for exactly this reason.
- Cache size versus application memory: the cache uses free RAM, so it shrinks when programs need memory. A memory-hungry program can push hot file pages out and slow every later read.
- Reclaim policy: preferring to evict clean pages keeps eviction cheap, but if the working set is mostly dirty, eviction stalls on writebacks no matter what the policy prefers.
Variations and controls
The basic mechanism has several knobs and cousins worth knowing, because they show up in real tuning and interview follow-ups.
- Read-ahead: on a sequential read the kernel prefetches upcoming pages, so misses turn into hits before you ask. It backs off when access looks random.
- Write-back tuning: on Linux, vm.dirty_background_ratio starts background flushing when dirty pages pass a low-water mark, and vm.dirty_ratio blocks writers when they pass a high one, so a burst cannot fill RAM with un-flushed data.
- fsync and O_DIRECT: fsync forces a writeback and waits, giving durability. O_DIRECT bypasses the page cache entirely, so a database can manage its own buffer pool and avoid keeping two copies of the same data.
- mmap: mapping a file makes its pages appear directly in a program's address space; reads and writes to that memory are page-cache reads and writes, with the same clean/dirty and writeback behaviour underneath.
- Dropping the cache: on Linux, writing to /proc/sys/vm/drop_caches frees clean cached pages (dirty ones are flushed first), which is mostly a benchmarking tool for measuring cold-cache performance.
Page cache versus other caches
It helps to place the page cache next to the caches it is often confused with. All three keep hot data close, but they differ in what they cache and whether they carry the dirty-page burden.
| Cache | Caches what | Eviction | Dirty pages? |
|---|---|---|---|
| OS page cache | File pages in kernel RAM | LRU, prefers clean | Yes β writeback owed |
| Application LRU cache | Objects / query results | LRU / LFU by policy | Usually no (or write-through) |
| Database buffer pool | DB pages, managed by the DB | LRU-ish, DB-controlled | Yes β its own writeback + WAL |
| CPU cache | Memory lines in silicon | Hardware, set-associative | Yes β dirty lines written back |
The LRU / LFU cache topic on this site models the pure eviction data structure: hit, miss, and evict the least-used entry. The page cache is that machine plus a durability obligation. Entries are not just present or absent; they are clean or dirty, and a dirty entry cannot leave without a trip to disk. A database buffer pool is the same idea again, layered on top of (or bypassing, via O_DIRECT) the OS page cache, with its own write-ahead log for crash recovery.
In the real world
The page cache is one of the most load-bearing pieces of any running system, and it is usually invisible until you go looking for it.
- On Linux, the 'buff/cache' column in free -m is the page cache. On a healthy server it is large β the kernel uses spare RAM for cached file pages and hands it back the moment a program needs it. High cache use is not a leak; it is the system working as designed.
- PostgreSQL deliberately keeps a modest buffer pool and leans on the OS page cache for the rest, so a page can live in both. Other databases (for example, some configurations of MySQL's InnoDB) use O_DIRECT to skip the page cache and avoid this double-buffering.
- The 'first run slow, second run fast' effect on builds, greps, and test suites is the page cache warming up: the first pass faults everything in from disk, and later passes hit RAM.
- Write-back tuning matters at scale. Set the dirty ratio too high and a machine can accumulate gigabytes of un-flushed writes, then stall hard when it finally flushes them all at once β a classic latency spike.
Common pitfalls
- Assuming a completed write() is durable. It is only in RAM until a writeback. If durability matters, call fsync and check that it succeeded; a crash before the flush loses the data.
- Reading high cache usage as a memory problem. Cached file pages are reclaimable β the kernel drops clean ones instantly under pressure. 'Low free memory' with large cache is normal and healthy.
- Benchmarking with a warm cache. If you do not drop caches or account for warm-up, your second run measures RAM, not disk, and your numbers lie about cold performance.
- Ignoring the dirty ceiling. A write-heavy job that outruns the disk fills the dirty budget and then throttles to disk speed anyway, often in a sudden stall rather than a smooth slowdown.
- Double caching by accident. Running a database that manages its own buffer pool without O_DIRECT can keep two copies of the same page (one in the DB, one in the page cache), wasting RAM.
In an interview
What is the difference between a clean and a dirty page?
A clean page's RAM copy matches disk exactly, so it can be evicted for free by just dropping the frame. A dirty page holds writes that disk has not seen yet, so it is the only current copy β it must be written back to disk before its frame can be reused, or the data is lost.
Why do writes return before the data is on disk?
The write only updates the page in RAM and marks it dirty; the disk update is deferred to a later writeback. This makes writes fast and lets many writes to the same page batch into one disk write. The cost is a durability window: an un-flushed write is lost on a crash unless you fsync.
The cache is full and you need to bring in a new page. What happens?
The kernel evicts a page, roughly the least-recently-used one, preferring a clean victim. A clean page is dropped for free. If it must evict a dirty page, it writes that page back to disk first, then frees the frame β an extra disk write on the eviction path.
How is the page cache different from an application-level LRU cache?
An LRU cache tracks presence and recency: hit, miss, evict the least-used entry. The page cache adds a durability obligation on top β entries are clean or dirty, and a dirty one carries a pending disk write. So eviction is not uniformly cheap; it depends on whether the victim is dirty.
How would you guarantee a write survives a crash?
Call fsync (or open with O_SYNC), which forces a writeback of the file's dirty pages and waits for the disk to confirm. Until that returns, the write is only in the volatile page cache. Databases build their durability guarantees on exactly this, paired with a write-ahead log.
References
- Linux kernel docs β VM sysctls (dirty_ratio, dirty_background_ratio) β The knobs that bound how many dirty pages accumulate and when writeback kicks in.
- Linux kernel docs β Page cache overview β How the kernel structures memory, including the page cache and reclaim.
- LWN β Writeback and the flusher threads β How dirty pages get flushed to disk in the background.
- PostgreSQL docs β Resource consumption and the OS cache β Why Postgres deliberately relies on the operating system's page cache.
- Bovet & Cesati β Understanding the Linux Kernel, ch. 15 (The Page Cache) β The classic book-length treatment of the page cache and writeback.
Ready to try it?
The simulator is a real, deterministic implementation β pick a scenario and step through it, scrubbing the timeline forward and backward through every change.