PostgreSQL’s default configuration assumes a machine with far more RAM than a small server provides. On a 1 GB or 2 GB instance, out-of-the-box settings can push the database into swap within a day of moderate traffic, and swap thrashing on a shared virtual disk turns a 5 ms query into a 2-second one. This is a practical tuning walkthrough for memory-constrained PostgreSQL deployments.
Baseline: What Each Setting Costs
| Setting | Default | Memory behaviour | Sensible small-server value |
|---|---|---|---|
shared_buffers | 128 MB | Fixed allocation at startup | 25% of RAM, e.g. 256 MB on 1 GB |
work_mem | 4 MB | Per sort/hash operation, can multiply | 4-8 MB; keep low on shared boxes |
maintenance_work_mem | 64 MB | Per maintenance connection | 64-128 MB |
effective_cache_size | 4 GB | Planner hint only, no allocation | 50-75% of RAM |
max_connections | 100 | Each backend ~5-10 MB | 20-40, with a connection pooler |
wal_buffers | -1 (auto) | Small fixed buffer | Leave default or 16 MB |
The dangerous settings are the per-operation ones. work_mem is allocated per sort or hash node, per connection. With work_mem = 64MB and 20 concurrent sort-heavy queries, PostgreSQL will attempt 1.28 GB of work memory on a 1 GB box. That is the classic path to swap death, and it happens gradually enough that people blame the provider instead of the config.
The Tuning Sequence
- Measure current usage. Run
free -mandps aux --sort=-rss | headto see actual PostgreSQL RSS. Checkvmstat 1 10— a non-zerosi/socolumn during normal traffic means you are already swapping. - Check the log for spills. Set
log_temp_files = 0to log every temp file larger than 0 bytes. Frequent temp files meanwork_memis too small for your queries; one or two large ones mean a single bad query. - Set shared_buffers to 25% of RAM. Edit
postgresql.conf, restart, and confirm withSHOW shared_buffers;. - Set max_connections to 20-40 and deploy a pooler. PgBouncer in transaction mode lets 200 application clients share 20 database backends — the single largest memory saving available.
- Cap work_mem conservatively. Multiply expected concurrent sort operations by
work_memand confirm the product stays under 20% of RAM. - Set effective_cache_size to 60% of RAM. This is planner guidance only — it costs nothing and improves join planning.
- Restart and re-measure. Compare
vmstatswap activity and query latency before and after.
Change one group of settings at a time. If you alter all seven at once and performance improves, you learn nothing about which change mattered, and you cannot safely tune the next server.
Sizing Guide by Instance RAM
| Instance RAM | shared_buffers | work_mem | max_connections | effective_cache_size |
|---|---|---|---|---|
| 1 GB | 256 MB | 4 MB | 20 | 512 MB |
| 2 GB | 512 MB | 6 MB | 30 | 1 GB |
| 4 GB | 1 GB | 8 MB | 40 | 2.5 GB |
| 8 GB | 2 GB | 16 MB | 60 | 5 GB |
Kernel and OS Level Adjustments
- Swap on SSD, not a spinning or network volume. If your provider gives you
swapon --showoutput with a slow device, keep swap minimal rather than large. - Lower
vm.swappinessto 10 so the kernel keeps the database in RAM instead of eagerly swapping idle pages. - Consider
vm.overcommit_memory = 2with a modest overcommit ratio on dedicated boxes — it prevents the OOM killer from choosing random victims, though it can cause allocation failures instead. - Install pg_stat_statements before tuning further; it adds small overhead but is the only reliable way to find the queries causing temp spills.
- Review autovacuum — a table bloated by stale rows reads more pages and burns more buffer cache than a freshly vacuumed one.
When Tuning Is Not Enough
If pg_stat_statements shows a handful of queries causing multi-gigabyte sorts, the fix is an index or a rewritten query, not more RAM. Add the index, confirm the temp file count drops, and only then reconsider your plan size. Many deployments chase a plan upgrade when an index would have solved the problem for zero dollars.
If you do conclude the instance is genuinely too small, size the upgrade against measured peak memory rather than guessing. You can compare budget VPS plans on our comparison table to see which tiers give you the RAM headroom you need, and our guide to RAM headroom on a VPS explains how much spare memory to keep on a database server. For swap-specific diagnostics, our analysis of swap versus RAM covers how to tell a quiet memory shortage from a real one.
Run the tuning loop once a quarter, or after any traffic step-change. Configuration is not a one-time task — it is a habit that keeps a small database server fast without buying more hardware than you need.
Monitoring: Catching Swap Thrash Before Users Do
Tuning is static; monitoring is what keeps it working. Three cheap signals tell you a small PostgreSQL server is under memory pressure before queries slow down visibly. First, watch si and so in vmstat — sustained non-zero values mean active swapping, not just a loaded swap file. Second, track the count of temp files logged by log_temp_files; a rising count means queries are spilling to disk. Third, watch the OS page cache size with free -m — if the cache is being squeezed to near zero, the database has outgrown the instance.
Set a simple alert on any of these and you get days of warning instead of a sudden outage. A cron job writing vmstat and pg_stat_activity summaries to a log, plus a weekly glance at the trend, is enough for most small deployments. The goal is to make the memory ceiling visible long before the OOM killer makes it obvious.
A Realistic Before-and-After
A typical 1 GB instance running PostgreSQL with default settings might show 200 MB of swap in use during peak traffic and a p99 query latency near 800 ms. After capping work_mem at 4 MB, adding PgBouncer, and setting shared_buffers to 256 MB, the same workload often drops to near-zero swap and a p99 in the low tens of milliseconds — on identical hardware, for no additional money. That gap is why tuning before upgrading is almost always the correct order of operations.


