PostgreSQL on a VPS: sizing memory, disk and connections

A fast PostgreSQL database is one whose frequently read data fits in memory. All the sizing follows from that: how big the database is, how much of it is really in use, how many connections it serves, then the disk and its headroom. This guide gives the method, starting settings for 4, 8 and 16 GB of memory, and the queries that measure an existing database.

Published on 29 October 2026 By 10-minute read

All guides

In short

  1. Measure the size of the database and its indexes, then the part that is read often: that is what has to fit in memory.
  2. Starting point: shared_buffers at 25% of RAM, effective_cache_size at 75%, random_page_cost = 1.1 on NVMe SSDs.
  3. Every connection is a process: beyond a few dozen active connections, put PgBouncer in front of the database.
  4. Disk: data and indexes a year from now, plus at least 30% headroom. A full disk stops PostgreSQL.
  5. The cache hit ratio should stay above 99%: below that, the database is short of memory.
  6. Back up daily with pg_dump, off the server, and restore regularly to prove it works.

Start from the working set

On a database server, memory is first of all a cache: a page read from memory is served far faster than one read from disk, even NVMe. So the question is not only "how big is the database?" but "how much of it is read often?". A 50 GB database where only the last twelve months are queried may be working on 8 GB.

On an existing database, two queries give the total size and the largest tables, indexes included:

SQL (psql)
SELECT pg_size_pretty(pg_database_size(current_database()));

SELECT relname AS table_name,
       pg_size_pretty(pg_total_relation_size(relid)) AS size_with_indexes
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 10;

For a database yet to be built, estimate table by table: rows expected after a year × average row size, then add indexes, which commonly weigh 30 to 100% of the data. Example: 2 million rows a month at 500 bytes each make 12 GB of data after a year, and 18 to 24 GB with indexes.

Memory: starting settings

PostgreSQL keeps pages it has read at two levels: its own cache (shared_buffers) and the operating system’s file cache. Its documentation recommends starting shared_buffers at 25% of memory on a dedicated server, and notes that going beyond 40% rarely helps: the rest serves the OS cache and sorting.

Setting4 GB8 GB16 GBWhat it does
shared_buffers1GB2GB4GBPostgreSQL’s own cache
effective_cache_size3GB6GB12GBThe estimated total cache, which guides query planning
work_mem8MB16MB32MBMemory for each sort or hash, per connection: multiply by concurrent queries
maintenance_work_mem256MB512MB1GBIndex builds and VACUUM
random_page_cost1.11.11.1The estimated cost of a random read: 4 by default, far above the real cost on NVMe

These values assume the database is alone on the server. If the application runs alongside it, subtract the app’s memory first (processes, cache, builds) and apply the percentages to what is left: an 8 GB server running a 3 GB application is tuned like a 5 GB database server.

postgresql.conf, for an 8 GB server dedicated to the database
shared_buffers = 2GB
effective_cache_size = 6GB
work_mem = 16MB
maintenance_work_mem = 512MB
random_page_cost = 1.1

shared_buffers only takes effect when PostgreSQL restarts; the other settings reload without interruption, with SELECT pg_reload_conf();.

To see whether memory is enough, read the cache hit ratio. For a business application or a website, it stays above 99%:

SQL (psql)
SELECT round(100.0 * blks_hit / nullif(blks_hit + blks_read, 0), 2) AS cache_hit_ratio
FROM pg_stat_database
WHERE datname = current_database();

Connections: one process each

PostgreSQL starts one process per connection and accepts 100 by default (max_connections). Each process takes a few megabytes when idle, more when it sorts or hashes data (work_mem, sometimes several times within one query). Raising max_connections to 500 to keep up with a chatty application exposes the server to running out of memory at the first peak.

The right answer is a connection pooler: PgBouncer in transaction mode lets hundreds of application connections share a few dozen real ones. Frameworks also have their own pool (SQLAlchemy, Prisma, HikariCP): size it on the database server’s vCPU count rather than on the number of users.

Disk: size, headroom and the WAL

The disk of a PostgreSQL server holds more than tables:

The rule: size after a year, plus at least 30% headroom. It is not a comfort margin: when the disk holding the WAL fills up, PostgreSQL shuts down to protect its data. Watch disk usage, and grow before 80%.

On performance, every commit waits for the WAL to be written to disk: storage latency sets write throughput. That is where an NVMe SSD makes the difference, far more than on reads, which memory already serves.

CPU: one query, one core

A PostgreSQL query runs on a single core, except for large scans that the planner parallelizes. The vCPU count therefore sets how many heavy queries run at once: 2 vCPUs are enough for a business application at launch, 4 to 8 suit busy databases and heavy reporting.

Before adding vCPUs, find the queries that use them. The pg_stat_statements extension ranks them by total time:

postgresql.conf, then restart
shared_preload_libraries = 'pg_stat_statements'
SQL (psql)
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

SELECT calls, round(total_exec_time) AS total_ms,
       round(mean_exec_time::numeric, 1) AS mean_ms, query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

Same server as the app, or a separate one?

At launch, running the database on the same server as the application is the right call: no network latency between them, a Unix socket connection, one server to back up and monitor. Split them when they compete for resources: a build or a traffic spike that flushes the database cache, a database that needs all the memory for its data, or different backup requirements.

Security and backups

Terminal: compressed export, then restore into a test database
pg_dump -Fc -d my_db -f my_db-$(date +%F).dump
createdb my_db_test
pg_restore -d my_db_test my_db-2026-10-29.dump

A fast PostgreSQL database, on the right server

VPS Pro runs on Intel Xeon processors, ECC memory that corrects errors on the fly and mirrored NVMe SSDs, with every data block verified by checksum and an encrypted backup every night, stored off the machine. Full-Stack Assistance is included: when your database slows down, we analyze the slow queries and indexes, then tell you which settings will bring your application back up to speed.

Explore VPS Pro

Frequently asked questions

How much RAM does PostgreSQL need?

Enough for frequently read data to fit in memory. For a business application at launch, 4 GB is enough, database and app included. After that, watch the cache hit ratio: as long as it stays above 99%, memory is sufficient.

What should shared_buffers be set to?

25% of memory on a server dedicated to the database, as the PostgreSQL documentation recommends: 1 GB on 4 GB, 2 GB on 8 GB, 4 GB on 16 GB. Beyond 40%, the gain becomes rare, since the operating system cache does the rest.

How many PostgreSQL connections can a VPS handle?

PostgreSQL accepts 100 by default, and each connection is a process that uses memory. Rather than raising that ceiling, put a pooler such as PgBouncer in front of the database: a few dozen real connections then serve hundreds of application connections.

Should the database run on a separate server?

Not at launch: on the same server, the database answers with no network latency and everything is backed up together. Split them when one gets in the way of the other, for example when a build or a traffic spike flushes the database cache.

Contact us