Blog · Performance

Five Postgres settings we change on every new cluster

PostgreSQL's defaults are built to start on almost any machine, not to run production. Here are the five settings we change first, and why.

KP
keenPG EngineeringPerformance team · 7 min read
The short version
  1. shared_buffers: from 128 MB to about a quarter of the server's memory.
  2. effective_cache_size: tell the planner how much memory really caches your data.
  3. work_mem: a little more memory for sorts and hashes, carefully.
  4. random_page_cost: tell the planner your disks are SSDs.
  5. max_wal_size: fewer, calmer checkpoints.

PostgreSQL ships with defaults that will start on a Raspberry Pi. That's a feature: it runs anywhere. But it means a fresh install on a 64 GB production server is using a tiny fraction of the machine, and the query planner is making decisions for hardware from another decade.

These are the five settings we look at first on every new cluster. None of them is magic, and every workload is different, so treat the numbers below as starting points to measure from, not rules.

You can change all of them with ALTER SYSTEM, which writes to postgresql.auto.conf, followed by a reload or, where noted, a restart.

Memory

1 · shared_buffers

shared_buffers is PostgreSQL's own cache of table and index pages. The default is 128 MB, regardless of how much memory the server has.

A common starting point is about 25% of the server's RAM. PostgreSQL also relies on the operating system's file cache, which is why we don't give it most of the memory: the two caches work together.

sql
ALTER SYSTEM SET shared_buffers = '16GB';  -- on a 64 GB server
-- needs a restart to take effect

2 · effective_cache_size

This one doesn't allocate anything. It's a hint to the query planner about how much memory is available for caching data overall, PostgreSQL's cache plus the operating system's. With a realistic value, the planner is more willing to use index scans.

The default is 4 GB. Somewhere between 50% and 75% of RAM is typical.

sql
ALTER SYSTEM SET effective_cache_size = '44GB';  -- on a 64 GB server
SELECT pg_reload_conf();

3 · work_mem

work_mem is the memory a single sort or hash operation may use before spilling to temporary files on disk. The default is 4 MB, so large sorts, ORDER BY, DISTINCT and hash joins often spill.

Be careful: this is per operation, not per connection. One complex query can run several sorts and hashes at once, and every connection can do it at the same time. A rough sanity check is:

sql
-- worst case, roughly:
-- max_connections × (operations per query) × work_mem   must fit in RAM

We usually start between 16 MB and 64 MB, then look for queries that still spill (see the bonus section below) and raise work_mem only for those, per role or per session:

sql
ALTER ROLE reporting SET work_mem = '256MB';  -- just for the reporting user
The planner

4 · random_page_cost

The planner estimates how expensive it is to read pages in random order (as an index scan does) compared with reading them in sequence. The default random_page_cost is 4.0, which made sense for spinning disks, where random reads really were much slower.

On SSD or NVMe storage, random reads cost almost the same as sequential ones. With the default value, the planner can wrongly prefer a full table scan over a perfectly good index. We set it close to the sequential cost:

sql
ALTER SYSTEM SET random_page_cost = 1.1;
SELECT pg_reload_conf();
Writes

5 · max_wal_size

Every change in PostgreSQL is first written to the write-ahead log (WAL). A checkpoint then flushes changed pages to the data files, so the old WAL can be recycled. Checkpoints happen on a timer (checkpoint_timeout, 5 minutes by default) or when the WAL reaches max_wal_size, which defaults to 1 GB.

On a busy database, 1 GB of WAL can fill up in well under five minutes. Then checkpoints happen back to back, writes come in bursts, and right after each checkpoint every changed page has to be written to WAL in full again, which creates even more WAL.

Raising max_wal_size lets checkpoints happen on the timer instead, spread out and calm:

sql
ALTER SYSTEM SET max_wal_size = '8GB';
SELECT pg_reload_conf();

You can see whether checkpoints are triggered by the timer or forced by WAL volume in the checkpoint statistics (pg_stat_bgwriter, or pg_stat_checkpointer in PostgreSQL 17). If most of them are requested rather than timed, raise max_wal_size further. checkpoint_completion_target (0.9 by default in current versions) already spreads the writes across the interval.

Bonus: see your slow queries

Tuning without measuring is guessing. Two things we turn on everywhere:

sql
-- every statement's runtime, call count and I/O, aggregated
ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements';  -- restart needed
CREATE EXTENSION pg_stat_statements;

-- log any query slower than half a second
ALTER SYSTEM SET log_min_duration_statement = '500ms';

-- log sorts and hashes that spill to disk (helps tune work_mem)
ALTER SYSTEM SET log_temp_files = 0;
SELECT pg_reload_conf();

Then the most expensive queries are one query away:

sql
SELECT round(total_exec_time) AS total_ms, calls, round(mean_exec_time, 1) AS mean_ms, left(query, 70) AS query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
Want a second pair of eyes on your config?A keenPG health check reviews every setting against your real workload.
See keenPG Experts

The settings we don't touch on day one

It's just as important to know what to leave alone until you have measurements: max_connections (use a connection pooler instead of raising it), the autovacuum settings for individual tables (see our post on autovacuum), and parallel query settings. Start with the five above, measure, and change one thing at a time.

KP
keenPG Engineering

Written by the engineers who set up and tune PostgreSQL clusters, and who have opened postgresql.conf more times than they can count.