shared_buffers is Postgres’s primary memory cache, and getting it right can be the single biggest performance win, but most people get it wrong by making it too small.

Let’s watch it in action. Imagine you have a PostgreSQL database with a table users that’s 100MB.

-- Create a dummy table
CREATE TABLE users (
    id serial PRIMARY KEY,
    name varchar(255),
    email varchar(255)
);

-- Populate it to ~100MB
INSERT INTO users (name, email)
SELECT 'User ' || i, 'user' || i || '@example.com'
FROM generate_series(1, 5000000) AS i;

-- Check the size
SELECT pg_size_pretty(pg_total_relation_size('users'));
-- Output will be around 100MB

Now, let’s say your shared_buffers is set to a default of 128MB.

-- Check current setting
SHOW shared_buffers;
-- Output: 128MB

When you query this table, PostgreSQL will try to load as much of it as possible into shared_buffers.

-- First query might be slow
SELECT * FROM users WHERE id = 1;

The first time you read data from users, it’s on disk. PostgreSQL reads it from disk and places it into shared_buffers. Subsequent reads of the same data will be lightning fast because they come directly from RAM.

If shared_buffers is too small, say 128MB for our 100MB users table, PostgreSQL can only hold a portion of it. When you access different parts of the users table, PostgreSQL might have to evict older data from shared_buffers to make room for new data, leading to more disk reads.

The problem PostgreSQL solves is that disk I/O is orders of magnitude slower than memory access. By caching frequently used data in RAM, PostgreSQL dramatically reduces the need to go to disk. shared_buffers is the dedicated area in RAM for this caching. It’s shared by all backend processes, hence the name.

The core levers you control are the size of shared_buffers and how PostgreSQL manages its replacement policy (which blocks to keep and which to evict). The default replacement policy is a variant of LRU (Least Recently Used), which is generally effective. The key is simply giving it enough space.

Here’s how you tune it:

  1. Determine your RAM: Know how much total RAM your database server has.
  2. Allocate for OS and other processes: Don’t give all RAM to PostgreSQL. The OS needs RAM, and other PostgreSQL components (like work_mem, WAL buffers, connection overhead) also consume memory. A common starting point is to dedicate 25% of total RAM to shared_buffers if you have ample RAM (e.g., 64GB+), or up to 40% if RAM is more constrained or if your workload is heavily read-bound.
  3. Set shared_buffers: Edit your postgresql.conf file. For a server with 64GB of RAM, you might set:
    shared_buffers = 16GB
    
    If you have 32GB, you might try:
    shared_buffers = 8GB
    
    Or even:
    shared_buffers = 12GB
    
    The value must be a multiple of wal_segment_size (typically 16MB) or 1MB if wal_segment_size is not set. A common mistake is setting it to a non-standard value that Postgres rejects.
  4. Restart PostgreSQL: This is a "hot" parameter, but for shared_buffers, it requires a full restart to allocate the memory.
    sudo systemctl restart postgresql
    
  5. Monitor: Observe cache hit ratios and query performance. A good hit ratio (above 95%) indicates shared_buffers is effectively caching data. You can check this with:
    SELECT
        sum(blks_hit) AS hits,
        sum(blks_read) AS reads,
        sum(blks_hit) / (sum(blks_hit) + sum(blks_read)) AS hit_ratio
    FROM pg_stat_database
    WHERE datname = current_database();
    

The most impactful shared_buffers size is often the one that can hold your entire working set of data. If your most frequently accessed tables and indexes fit entirely within shared_buffers, you’ll see a dramatic performance improvement because almost all reads will be cache hits. Many systems with 64GB or more RAM can afford to set shared_buffers to 16GB, 32GB, or even more, which is far beyond the typical 128MB or 256MB defaults.

After tuning shared_buffers, you’ll likely start thinking about how PostgreSQL manages writes to disk, which leads to tuning wal_buffers and max_wal_size.

Want structured learning?

Take the full Postgres course →