Ask why PostgreSQL has shared_buffers and InnoDB has a buffer pool when Linux already caches file pages, and you’ve asked one of the best questions in systems design — because the answer isn’t “legacy.” Every serious database re-implements caching on top of or instead of the kernel’s, and the reasons illuminate exactly where general-purpose OS policies stop being good enough.

The Page Is the Atom

Storage engines see tables as arrays of fixed-size pages (8 KB Postgres, 16 KB InnoDB) — the unit of I/O, locking granularity, WAL reference, and caching. Inside, the classic slotted layout: header, a slot directory growing forward, tuples growing backward, so variable-size rows can be shuffled within a page without external pointers changing (slot indirection is the whole trick; it’s also why a “tiny” one-column update dirties an entire page). The buffer pool is a big array of page-sized frames plus a hash table (page id → frame) and per-frame metadata: pin count (in use — unevictable), dirty flag, and usage tracking for eviction. Every read pins, uses, unpins; every miss picks a victim, evicts (writing it first if dirty), and reads in.

Why Not Just the Page Cache?

The database knows things the kernel structurally can’t:

  • Access semantics. A sequential scan of a huge table would flush the entire cache under naive LRU — one query evicting your working set. Databases special-case it (Postgres ring buffers for scans, InnoDB’s midpoint insertion: new pages enter 3/8 from the tail and must be re-touched to reach the hot end; classic clock-sweep with usage counts elsewhere). The kernel sees anonymous reads and can’t distinguish your OLTP index root from a batch scan’s confetti.
  • Write ordering. WAL discipline requires the log flushed before a dirty data page reaches disk, checkpoints require controlled flush scheduling — the kernel’s writeback flushes whatever, whenever. A database must at minimum control its own dirty-page destiny.
  • Double caching and double copying. Buffered I/O keeps every hot page twice (pool + page cache) and copies on each miss. Hence O_DIRECT: InnoDB defaults to it and owns the whole path; Postgres, notably, still rides the page cache (double-caching is why the standard advice is shared_buffers ≈ 25% of RAM and let the kernel hold the rest, and why effective_cache_size exists to tell the planner about the kernel’s share) — an ongoing architectural debate you can watch in the async-I/O work landing in recent releases.

The through-line generalizes past databases: bypass the OS when you have semantic knowledge (future access patterns, ordering constraints) that its interface can’t express — and pay the price of reimplementing readahead, writeback scheduling, and eviction yourself. Kafka is the perfect counter-example that proves the rule: its access pattern (sequential log append/tail-read) is exactly what the page cache is already optimal for, so it deliberately owns no cache and that’s equally principled.

Reading the Pool in Production

The metrics that matter map straight to the mechanics. Hit ratio (pg_stat_database.blks_hit/blks_read, InnoDB Buffer pool hit rate) — with the caveat that Postgres’s number counts kernel-cache hits as “reads,” flattering nobody. Dirty/ flush pressure: checkpoint-time write storms and “buffer pool wait free” conditions mean writeback can’t keep pace — tune background writers before buying disks. Pin/latch contention on a handful of pages (the root of a hot index, a counter row) shows up as buffer-content lock waits and is a schema problem wearing a cache costume. And when sizing: bigger pools help until the working set fits, then flatten — while making checkpoints heavier and restart warmup longer (hence pg_prewarm and InnoDB’s buffer-pool dump/restore, which exist because a cold 200 GB pool after failover is its own incident).

Takeaways

  • Fixed-size slotted pages are the atom of I/O, logging, and locking; the buffer pool is a hash-indexed frame cache with pins, dirty flags, and clock-ish eviction.
  • Databases self-cache for semantics the kernel can’t see: scan resistance, WAL-before-data ordering, checkpoint control.
  • O_DIRECT (InnoDB) vs ride-the-page-cache (Postgres) is a live design fork — the 25%-of-RAM rule is double-caching arithmetic, not superstition.
  • The bypass principle: own the path only when you know more than the OS and can afford to reimplement its machinery (Kafka rightly doesn’t).
  • Watch hit ratios skeptically, flush pressure seriously, and hot-page contention as the schema smell it is; prewarm after failovers.