← All posts

Why adding more shared_buffers doesn't always help

Giovanni Martinez·June 2, 2026·4 min read
postgresqlshared_buffersperformance tuningmemory managementdatabase performanceos page cachequery optimizationdba


This is the first piece in a series on PostgreSQL memory. I want to start with a moment that happens in some version every few months somewhere in the Postgres community.

A junior engineer looks at htop on the database server and notices something alarming: out of 32 GB of RAM, Postgres is using barely 5%. The rest is sitting there, apparently idle. The natural conclusion: bump shared_buffers to 75% and put all that memory to work.

I explained why this would have been a mistake. I don't think I explained it as well as I wanted to at the time. Here's the version I wish I'd given.

Misconception #1: "Postgres is only using 5% of the RAM."

It isn't. The 5% you see in htop is the memory Postgres has directly allocated for itself — shared_buffers plus per-backend memory. What's missing from that picture is the OS page cache.

When Postgres reads a data file, the OS caches that file in its page cache. The next read of the same data doesn't touch disk — it comes from the OS cache, which is just as fast as shared_buffers from a query's perspective. On a healthy Postgres server, the OS page cache is doing enormous work that's invisible in htop's "memory used by Postgres" column.

So when htop says Postgres is using 5% and the OS is using 70%, the truthful picture is: Postgres is benefitting from ~75% of the RAM. The OS is just the one holding it.

There's also a second thing happening here. Postgres only uses what it needs at any given moment. shared_buffers is a ceiling, not a target. If Postgres is configured for 32 GB of shared_buffers but the workload's working set fits in 4 GB, the buffer pool will fill up to roughly that working set and stay there. The remaining capacity sits available but unused. That's not waste — it's headroom.

This is where the nuance lives. If Postgres is consistently using only 5% and your cache hit ratio (we'll get to that below) is below 99%, then yes, you probably do have room to grow shared_buffers — just not to 75%. The honest answer is somewhere between the 5% the server has now and the 25–40% range we'll cover in the next section. Memory tuning is rarely a single dramatic move; it's usually a small adjustment guided by what the database is actually doing.

Misconception #2: "More shared_buffers means faster queries."

This is true up to a point, then it stops being true, then it gets actively counterproductive. Three reasons:

Double-buffering. Hot data ends up in both shared_buffers and the OS page cache. Past a certain size, you're storing the same bytes twice and wasting RAM that could be doing other work.

More overhead, not less. A bigger buffer pool means more bookkeeping. The buffer manager has more to scan when evicting pages, more LWLock contention on hot pages, and a longer tail on certain operations.

You're starving everything else. Postgres needs RAM for work_mem (sorts, hashes), maintenance_work_mem (vacuum, index builds), connections, and the OS itself. Crank shared_buffers to 75% and you've squeezed all of those.

The empirically reasonable range is 25% to 40% of RAM, depending on the workload. 25% is the safe starting point. 40% is the soft upper bound for read-heavy OLTP workloads with well-defined hot sets. Past 40%, you're almost always trading useful OS-page-cache RAM for diminishing returns.

What to look at instead of just cranking the value

The diagnostic that actually matters is the buffer cache hit ratio:

SELECT
sum(blks_hit) * 100.0 / nullif(sum(blks_hit) + sum(blks_read), 0)
AS cache_hit_ratio
FROM pg_stat_database;

If this is above ~99%, your shared_buffers is sized appropriately for your working set. If it's lower, you have a real cache-pressure problem — but the answer might not be "more shared_buffers." Sometimes it's:

  • Fix a query that's doing sequential scans on a large table when it should be using an index
  • Add or reorganize an index so the working set shrinks
  • For time-series workloads, consider whether the data shape itself is the problem — partition + compression strategies can dramatically reduce the working set you're trying to keep in cache

The mental model: shared_buffers isn't a faucet you turn up to "use the RAM." It's one tool in a memory system that includes the OS, your query patterns, and your schema design.

The series

This is the first in a series on PostgreSQL memory. The next issues will go deeper on work_mem (and why your sort spilled to disk), maintenance_work_mem (and why vacuum is crawling), and the OS page cache (where most of the real work happens).