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.
shared_buffers at 25% of RAM, effective_cache_size at 75%, random_page_cost = 1.1 on NVMe SSDs.pg_dump, off the server, and restore regularly to prove it works.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:
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.
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.
| Setting | 4 GB | 8 GB | 16 GB | What it does |
|---|---|---|---|---|
shared_buffers | 1GB | 2GB | 4GB | PostgreSQL’s own cache |
effective_cache_size | 3GB | 6GB | 12GB | The estimated total cache, which guides query planning |
work_mem | 8MB | 16MB | 32MB | Memory for each sort or hash, per connection: multiply by concurrent queries |
maintenance_work_mem | 256MB | 512MB | 1GB | Index builds and VACUUM |
random_page_cost | 1.1 | 1.1 | 1.1 | The 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.
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%:
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();
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.
The disk of a PostgreSQL server holds more than tables:
max_wal_size), more during a bulk import, and more still if WAL archiving falls behind.work_mem.VACUUM FULL and index rebuilds write a full copy before releasing the old one.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.
A PostgreSQL query runs on a single core, except for large scans that the planner parallelises. 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:
shared_preload_libraries = 'pg_stat_statements'
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;
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.
listen_addresses = 'localhost' as long as only the app on the server connects. If another server needs access, open port 5432 to its address only, in the firewall and in pg_hba.conf.scram-sha-256 method in pg_hba.conf, one role per application, without superuser rights.pg_dump, copied off the server.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
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 analyse the slow queries and indexes, then tell you which settings will bring your application back up to speed.
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.
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.
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.
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.
Simafri
Let's talk about your project
Tell us what you need in a few words: we will get back to you quickly.
Thank you! Your request has been sent. We'll get back to you shortly.
Prefer email? Write to us at support@simafri.com.