postgresql.conf by default is not broken, it is prudent: sized to run on a minimal machine without ever causing an installation to fail, not to handle your production traffic. Going from these compatibility values to production values is measured, it cannot be guessed. For the measurement protocol itself (realistic load, p50/p95/p99, reproducible results), see our backend benchmark methodology. This article details the settings themselves, in the order they matter most.
- Five projects, in order: connections/pooling, memory, autovacuum, indexes/slow queries, checkpoints/WAL.
max_connectionsandshared_buffersrequire a complete server restart; most other settings are hot-loaded.- Never disable autovacuum in production: the real risk is not slowness, it's transaction ID wraparound.
pg_stat_statementsmust be listed inshared_preload_librariesbefore a simpleCREATE EXTENSIONwill collect anything.- The Advisor integrated into Aurabase Studio already applies part of this checklist automatically: requests lasting more than 150 ms, foreign keys not indexed, connection pool beyond 80% saturation.
Why Postgres defaults are never enough
postgresql.conf, in its default state, is designed to never fail an installation, not to absorb your traffic. The historical value of shared_buffers, 128 MB, allows Postgres to start on a minimal machine without reserving any critical resources. max_connections at 100 fits on a small shared server. Neither was chosen for your actual load.
So the problem is not that Postgres is poorly set by default: it's that it was never set for you. The following sections go through the settings in the order they pay off the most, from the most common bottleneck (the connections) to the slowest to appear (the WAL).
Size max_connections and pooling before everything else
The first project is not memory, it is connections. Each Postgres connection opens a dedicated server process that consumes RAM and context CPU time, even if idle. Increasing max_connections to avoid "too many connections" type errors shifts the problem: beyond a certain number of simultaneous active connections, CPU contention degrades the latency of all requests, including the fastest.
The correct approach reverses the usual order: scale max_connections on the actual server concurrency, then absorb the application-side concurrency with a pooler like PgBouncer into pool_mode=transaction. The pooler multiplexes hundreds of client connections onto a handful of actually active server connections. Our dedicated article details the transaction mode mechanism and its limits (prepared statements, LISTEN/NOTIFY), and a comparison between PgBouncer, Supavisor and PgCat to choose the implementation.
max_connections is a "postmaster" context parameter: changing it requires a full server restart, not a simple reload. On Aurabase dedicated Postgres instances (Pro and Enterprise plans, one CNPG cluster per project), this setting is configured per plan tier rather than left at its default value. A restart is not a trivial operation to repeat in production, which justifies this choice in stages rather than a fixed value. The formula for choosing your own value, with its limits, is the subject of a separate article: size max_connections.
Four memory settings weigh more than all the others combined: shared_buffers, effective_cache_size, work_mem and maintenance_work_mem. The first three determine how much data Postgres keeps in memory before returning to disk; the fourth determines the speed of a VACUUM or index creation.
shared_buffers sets the internal cache shared by all connections. The benchmark commonly documented by the PostgreSQL project is approximately 25% of the RAM available on a dedicated database server. Beyond that, the gains decrease and the operating system's disk cache takes over. effective_cache_size does not allocate anything: it is an estimate, given to the query planner, of the total memory available for the cache (Postgres and OS combined). Undersizing it pushes the scheduler towards sequential scans whereas an index would largely cache; the current benchmark is between 50 and 75% of RAM.
work_mem is the most common trap. This is not a global limit. Each sort or hash operation in a query can consume its own share of it, and a query with multiple joins can reserve its own share several times. Too generous a value combined with a high max_connections can exhaust server RAM under concurrent load, even if each request taken in isolation seems reasonable. maintenance_work_mem, conversely, can remain significantly more generous: it only applies to maintenance operations (VACUUM, CREATE INDEX), which are rarely concurrent with each other.
Only shared_buffers requires a reboot. The other three are hot reloaded, including for an isolated session: SET work_mem = '64MB'; for the duration of a single greedy request, without affecting the global setting.
Autovacuum: adjust the thresholds, never deactivate it
Never disable autovacuum in production, even temporarily to "free up resources" during a peak load. Postgres uses MVCC: each UPDATE and each DELETE leaves a dead line that only vacuum can recover. Without it, tables bloat, indexes degrade, and execution plans gradually deteriorate, with no errors visible until it's late.
The most serious danger of a disabled or undersized autovacuum is not performance, it is transaction ID wraparound. After a threshold, Postgres switches the entire database to read-only to avoid data corruption, until a manual VACUUM is executed. This is a production incident that is entirely avoidable through correct configuration.
The default value ofautovacuum_vacuum_scale_factor (20% dead rows before triggering) is suitable for a small table, not a write-intensive multi-million row table. On a table of 10 million rows, this 20% represents 2 million dead rows accumulated before the first pass. Lower this threshold table by table rather than changing the overall value of the entire database.
The Advisor integrated into Aurabase Studio checks this configuration at each project analysis, in the same way as tables without primary keys or non-indexed foreign keys. This is an explicit signal rather than a silent degradation discovered too late.
Index before adding RAM
The majority of latency problems in production come neither from the CPU nor from the RAM: they come from an absent or poorly chosen index. Before touching a single parameter of postgresql.conf, EXPLAIN (ANALYZE, BUFFERS) on the query in question remains the most reliable diagnosis. It reduces Postgres request latency well before adding resources.
An Seq Scan on a multi-million row table, where an Index Scan was expected, almost always signals an index problem. Three causes come up most often: a missing index, a column type incompatible with the existing index, or obsolete statistics after a massive import without ANALYZE. Adding RAM or increasing work_mem sometimes hides this symptom on a small volume of data; the problem reappears as soon as the table grows.
To locate these requests without searching them one by one, pg_stat_statements aggregates the execution statistics of all the server's requests. A common pitfall: the extension must first be listed in shared_preload_libraries, a "postmaster" context parameter which requires a restart. Without this step, CREATE EXTENSION pg_stat_statements; silently succeeds but collects nothing.
This is exactly the error that the Aurabase backend returns when this extension is absent: an explicit message rather than a silent empty list, which could be confused with "no slow query". The Studio Advisor goes further: it automatically classifies any request with an average time greater than 150 ms as warning, and beyond 500 ms as critical. These thresholds are based on the same pg_stat_statementsstatistics.
Checkpoints and WAL: smooth the load rather than suffer it
A checkpoint forces Postgres to write to disk all pages modified in memory since the previous one. By default, this writing may focus on too short a time window. The result is a noticeable spike in disk latency on the application side, the kind of periodic slowdown that's difficult to relate to a specific request.
checkpoint_completion_target controls the spread of this writing over the interval between two checkpoints. A detail missing from older checklists: PostgreSQL 14, released in 2021, changed its default value from 0.5 to 0.9. On a PostgreSQL 16 instance, such as dedicated Aurabase tenant clusters, this setting is therefore already correct by default; adjusting it manually only makes sense on a version prior to 14. See our comparison PostgreSQL 16 vs 17 vs 18 for other version changes that affect tuning.
max_wal_size acts in the same direction: a value that is too low triggers more frequent checkpoints than expected, even when checkpoint_timeout is not yet reached. Increasing it reduces the frequency of checkpoints, at the cost of longer recovery time after a crash since there are more WALs to replay. A compromise to be decided according to your tolerance for unavailability, not a universal value.
Monitoring is not a step, it is the loop that closes the checklist
This checklist is not a one-off audit to be checked off once before going into production. A base that doubles in volume or traffic that triples makes the benchmarks chosen at startup obsolete, often without explicit error, just a progressive degradation of p95 latency.
Three signals merit continued monitoring. pg_stat_statements identifies requests that degrade over time. pg_stat_activity reports blocked or abnormally long running queries, and the ratio of active connections to max_connections anticipates saturation before it produces application-side errors.
Studio's Observability tab covers part of this foundation for any Aurabase project, with no third-party tools to install. It lists slow requests, allows you to cancel or terminate an active request by PID, and displays a pool saturation indicator which goes into warning above 80% usage. On a self-hosted instance, this same monitoring is built by hand, with pg_stat_statements activated and an external monitoring tool plugged into it.
Cheat sheet: the complete checklist
Eight settings, in the order they pay off the most, with what you need to know before you touch them.
| max_connections | Reboot | Sized on real competition, not a round number; absorb the rest via a pooler in transaction mode. |
|---|---|---|
| shared_buffers | Reboot | ≈ 25% of RAM dedicated to Postgres. |
| effective_cache_size | Hot | ≈ 50 to 75% of RAM (Postgres + OS cache combined). |
| work_mem | Hot / session | Cautious by default; test upwards on a query-by-query basis with SET. |
| maintenance_work_mem | Hot | More generous than work_mem; speeds up VACUUM and CREATE INDEX. |
| autovacuum_vacuum_scale_factor | Hot, per table | Lower on large write-heavy tables, never globally. |
| checkpoint_completion_target | Hot | 0.9 by default since PostgreSQL 14; to check especially on an earlier version. |
| shared_preload_libraries | Reboot | Must include pg_stat_statements before any slow query analysis. |
The thresholds cited here, such as the 150 ms and 80% pool saturation that the Aurabase Studio Advisor monitors, are a code-verified starting point, not a universal truth. Your actual charge remains the sole final judge. To precisely size max_connections rather than following a general rule, the dedicated article details the formula and its limits.
FAQs
Three questions that systematically come up once the checklist is applied for the first time.