On this page
PostgreSQL performance tuning starts with five settings: shared_buffers, work_mem, the per-table autovacuum thresholds and cost limit, max_connections (fixed with a pooler, not a bigger number) and idle_in_transaction_session_timeout. Set each from measurement. Rank queries by total execution time in pg_stat_statements first, then change one setting at a time.
The expensive failures we see are rarely a missing setting. For an insurance services provider, autovacuum on the largest tables was being cancelled and restarted, long analytics sessions and idle-in-transaction connections pinned the oldest-transaction horizon, and cost-delay settings inherited from a much smaller deployment throttled autovacuum to a fraction of the disks' capacity. The database was within a few tens of millions of transactions of the point where PostgreSQL protects itself by going read-only. AceMQ froze the oldest tables first with targeted manual vacuums, ended the sessions pinning the horizon, retuned the cost settings to the hardware and set per-table thresholds. The cluster never went read-only. The write-up is in the wraparound remediation use case.
A second pattern: a retail technology provider saw response times drift upward over eighteen months and was about to buy hardware. An assessment ranked queries by total time, measured table and index bloat directly and profiled connections at the pooler. The constraint was query and bloat behavior, not capacity. The refresh was deferred and response times moved back toward their earlier baseline. See the PostgreSQL performance assessment.
Measure first: rank queries by total time
Add pg_stat_statements to shared_preload_libraries (a restart is required), run CREATE EXTENSION pg_stat_statements, and sort by total_exec_time, not by the slowest single call. A 3 ms query run 40 million times a day outranks a report that takes four seconds once. Save the top 50 as a baseline before any change, and read plans with EXPLAIN (ANALYZE, BUFFERS). The pg_stat_statements documentation lists the columns.
The 12 PostgreSQL settings and what they fix
Defaults are from the PostgreSQL 18 documentation. hash_mem_multiplier defaults to 1.0 on PostgreSQL 14 and 2.0 from 15, and autovacuum_vacuum_max_threshold exists only from 18.
| Setting | Default | Start here | What it fixes |
|---|---|---|---|
shared_buffers | 128MB | 25% of RAM on a dedicated server with 1GB or more; above 40% is unlikely to help. Restart required. | Hot data read from disk repeatedly |
effective_cache_size | 4GB | Shared buffers plus the OS cache you expect PostgreSQL to use. It reserves nothing. | Planner avoiding index scans on cached tables |
work_mem | 4MB | Raise for the roles that need it, not globally. Each sort or hash step can use this much. | Sorts and joins spilling to temp files |
hash_mem_multiplier | 2.0 | 2.0 to 8.0 or more once work_mem is 40MB or higher, per the documentation. | Hash joins and aggregates spilling |
maintenance_work_mem | 64MB | Well above work_mem. Each autovacuum worker can allocate this much unless autovacuum_work_mem is set. | Slow VACUUM and CREATE INDEX |
max_connections | 100 (typical) | Keep it modest and put a pooler in front. Restart required. | Per-connection resources multiplying |
autovacuum_vacuum_scale_factor | 0.2 | Lower it per table on large tables. | Dead tuples piling up before vacuum starts |
autovacuum_vacuum_max_threshold | 100,000,000 (18) | Leave it on 18, or emulate with a low per-table scale factor on 14 to 17. | Huge tables waiting for 20% churn |
autovacuum_vacuum_cost_limit | -1 (uses 200) | Raise it on fast storage. The limit is shared across running workers. cost_delay defaults to 2ms. | Autovacuum throttled below what the disks allow |
autovacuum_max_workers | 3 | Raise for many large tables, with the cost limit. On 18, autovacuum_worker_slots lets you change it without a restart. | Tables queued behind each other |
autovacuum_freeze_max_age | 200 million | Leave it; monitor age(). It can be lowered per table. | Forced anti-wraparound vacuums at peak load |
idle_in_transaction_session_timeout | 0 (off) | Set a finite value for application roles. | Open transactions blocking vacuum and causing bloat |
Worked example: sizing memory for a 64 GB server
This is an illustrative calculation, not a client result. Assume a dedicated 64 GB server, max_connections = 200, up to four sort or hash steps in a query, and the default hash_mem_multiplier of 2.0 (PostgreSQL 15 and later).
The work_mem budget is the memory left after shared_buffers and headroom, divided by connections × steps × the multiplier: (64 − 16 − 24) GB = 24,576 MB, and 24,576 ÷ (200 × 4 × 2.0) = 15.36, so 15MB. The 24 GB of headroom for the operating system, its page cache, maintenance and other processes is an assumption, not a documented figure. This is a worst case in which every connection runs four hash steps at once. A pooler that holds 50 active connections gives 24,576 ÷ (50 × 4 × 2.0) = 61 MB.
| Setting | Value | Arithmetic |
|---|---|---|
shared_buffers | 16GB | 25% of 64 GB |
effective_cache_size | 40GB | 62.5% of RAM, inside the usual 50 to 75% rule of thumb: 16 GB of buffers plus about 24 GB of OS cache |
work_mem | 15MB | 24,576 MB ÷ (200 × 4 × 2.0) |
maintenance_work_mem | 1GB | One manual VACUUM or CREATE INDEX at a time |
autovacuum_work_mem | 512MB | 3 workers (the default autovacuum_max_workers) × 512 MB = 1.5 GB worst case |
These are starting values to test, not targets. Compare temp_blks_written in pg_stat_statements and the plans from EXPLAIN (ANALYZE, BUFFERS) before and after, as above.
Autovacuum, bloat and transaction ID wraparound
Autovacuum starts on a table when dead tuples exceed autovacuum_vacuum_threshold (50) plus autovacuum_vacuum_scale_factor (0.2) times the table size. On PostgreSQL 17 and earlier a one-billion-row table therefore waits for about 200 million dead tuples; 18 caps the wait at 100 million. Override it per table:
ALTER TABLE orders SET (autovacuum_vacuum_scale_factor = 0.01, autovacuum_vacuum_cost_limit = 1000);The values are an example, not a recommendation: size them from the table's churn. For wraparound, the routine vacuuming documentation says PostgreSQL warns when the oldest IDs are 40 million transactions from the limit and refuses new IDs below three million. Watch the age:
SELECT datname, age(datfrozenxid) FROM pg_database;If age keeps rising, check the three blockers the documentation names: prepared transactions in pg_prepared_xacts, long-running transactions in pg_stat_activity and stale replication slots in pg_replication_slots. Once the server has stopped assigning IDs, run a database-wide VACUUM as a superuser; the documentation says not to use VACUUM FULL or VACUUM FREEZE in that situation.
Indexes and bloat
Vacuum that cannot remove dead tuples leaves bloat in tables and indexes. Check pg_stat_user_indexes for indexes with idx_scan of zero across a full business cycle, and drop the redundant ones; they cost writes and vacuum time for nothing. Rebuild a bloated B-tree with REINDEX INDEX CONCURRENTLY, which does not block writes but takes longer, and drop the leftover INVALID index if it fails. See the REINDEX documentation. In the retail technology assessment, redundant indexes and indexes the planner never chose were reviewed alongside the bloat measurements.
Connection pooling
max_connections is typically 100, and the documentation warns that a complex query can run several sort and hash steps at once, so total memory can be many times work_mem. Hundreds of application connections multiply that. Put a pooler such as PgBouncer in front and keep the server limit near what the CPUs can serve. In the retail assessment the application opened far more connections than the server had memory to serve efficiently, which was only visible when connections were profiled at the pooler.
Checking before and after a 14 to 17 upgrade
PostgreSQL 14's final release is on 12 November 2026, per the versioning policy. The 17 release notes describe a new vacuum memory structure and say vacuum is no longer limited to one gigabyte when maintenance_work_mem or autovacuum_work_mem is higher. The 18 release notes add asynchronous I/O, B-tree skip scans and eager freezing. Neither gives a speedup figure, so measure your own workload:
- Save
pg_stat_statementsand wraparound age before the upgrade. - After
pg_upgradeon 17 or earlier, regenerate planner statistics withvacuumdb --all --analyze-only; the 17 documentation says they are not transferred. PostgreSQL 18 preserves them, but not extended statistics. - Diff
postgresql.confagainst the old server so your autovacuum overrides survived. - Replay the same queries and compare calls, mean time and temp files per query ID.
When to bring in support
Bring in an outside engineer when a wraparound warning appears, autovacuum is cancelled repeatedly, a replication slot is holding back WAL, or a hardware purchase is proposed without evidence. AceMQ's PostgreSQL support is 24/7 with a named senior engineer, for self-managed and cloud-hosted PostgreSQL. For a one-off review, our PostgreSQL consulting starts with a PostgreSQL performance assessment. Version dates for 14 and later are in PostgreSQL end of life dates by version.
Sources
- PostgreSQL 18 documentation: Resource Consumption (shared_buffers, work_mem, hash_mem_multiplier, maintenance_work_mem)
- PostgreSQL 18 documentation: Automatic Vacuuming
- PostgreSQL 18 documentation: Routine Vacuuming (transaction ID wraparound)
- PostgreSQL 18 documentation: Query Planning (effective_cache_size)
- PostgreSQL 18 documentation: Connections and Authentication (max_connections)
- PostgreSQL 18 documentation: Client Connection Defaults (idle_in_transaction_session_timeout)
- PostgreSQL 18 documentation: pg_stat_statements
- PostgreSQL 18 documentation: REINDEX
- PostgreSQL 17 documentation: pg_upgrade
- PostgreSQL 14 documentation: Resource Consumption (hash_mem_multiplier default)
- PostgreSQL 17 release notes
- PostgreSQL 18 release notes
- PostgreSQL: Versioning policy, checked 22 October 2026
Frequently Asked Questions
How do I speed up a slow PostgreSQL database?
Find where the time goes before changing anything: enable pg_stat_statements and rank queries by total_exec_time. Then fix the biggest item, usually a missing or unused index, stale statistics, table bloat from blocked autovacuum, or too many connections. Change one thing at a time and compare the same queries afterwards.
What should shared_buffers be?
The PostgreSQL documentation suggests 25% of RAM as a starting value on a dedicated server with 1GB or more, and says more than 40% is unlikely to work better. The default is 128MB. Changing it needs a restart, so test the new value on a copy of the workload first.
Does upgrading PostgreSQL improve performance?
Often, but the release notes give no figures. PostgreSQL 17 reduced vacuum memory use and made B-tree searches over several values more efficient; 18 added asynchronous I/O and skip scans. Gains depend on your queries, so capture pg_stat_statements before and after. PostgreSQL 14 reaches end of life on 12 November 2026.
What is transaction ID wraparound?
PostgreSQL's 32-bit transaction IDs must be frozen by vacuum at least once every two billion transactions. If vacuum falls behind, PostgreSQL warns when the oldest IDs are 40 million transactions from the limit and stops assigning new IDs below three million, leaving only read-only transactions until vacuum catches up.
Talk to an engineer
Need a hand with PostgreSQL?
Senior engineers who run it in production. Get a quote or a free consultation, no sales pitch.

