If you are asking what size VDS you need for a production database and API, the answer for most small and medium applications is our VDS Small: 4 dedicated vCPUs, 8 GB RAM, 240 GB NVMe. In our lab on 29 October 2026, PostgreSQL 17.11 with a 755 MB pgbench dataset and a small FastAPI service on the same machine handled about 2,000 read-write transactions per second, 13,000 to 16,000 read-only transactions per second and roughly 2,400 to 2,800 API requests per second with only 2 to 4 vCPUs and 2 to 3 GB of RAM. Raw speed is rarely the reason to buy bigger. The reason to buy a VDS rather than a VPS for production is steady latency from dedicated vCPUs, and our measurements show what happens to a database when the host underneath it gets busy.
Key facts, checked on 29 October 2026:
- PostgreSQL 17.11, the current 17.x minor, was released on 13 August 2026 and is the version Debian 13 ships (PostgreSQL 17.11 release notes).
- The documentation suggests
shared_buffersof "25% of the memory in your system" on a dedicated database server with 1 GB or more of RAM, and says more than 40% is unlikely to help (resource consumption settings). max_connectionsdefaults to 100 andwork_memto 4 MB per sort or hash operation, not per connection (connection settings).- pgbench creates 100,000 rows in
pgbench_accountsper scale unit; scale 50 gave us a 755 MB database, scale 10 gave 157 MB (pgbench documentation, sizes measured in our lab). - The PostgreSQL wiki puts the best number of active connections near
(core_count * 2) + effective_spindle_count, with a spindle count of zero when the working data is cached (Number of Database Connections). - PgBouncer's
default_pool_sizeis 20 andmax_client_connis 100; in transaction mode a server connection is "released back to pool after transaction finishes" (PgBouncer configuration).
The short answer: traffic and data size to plan
"Working set" means the part of your data that is read often (recent orders, active users, hot indexes); that is what has to fit in RAM, not the whole database.
| Your situation | Plan | Why this one | City |
|---|---|---|---|
| Staging, development, demo; data under 1 GB | VPS Micro, 2 shared vCPU, 2 GB, 40 GB NVMe (Linux) | Our 2 GB test ran a 755 MB dataset entirely from RAM. Shared vCPUs are fine when nobody waits on p99. | Any; Prishtina adds free weekly backups |
| Small production API; data under 5 GB; a few hundred requests per second | VPS Mini, 4 shared vCPU, 4 GB, 80 GB NVMe | Room for 1 GB shared_buffers plus 4 API workers. Latency will move when neighbours get busy. | Any |
| Production database and API together; working set up to about 4 GB; up to roughly 1,000 requests per second | VDS Small, 4 dedicated vCPU, 8 GB, 240 GB NVMe, 10 Gb/s | The default production choice. Dedicated vCPUs give steady query times; 8 GB covers 2 GB shared_buffers, the OS cache, the API and PgBouncer. | Any; Prishtina for the included backups |
| Write-heavy or busy database; working set 4 to 10 GB; many connections or several databases | VDS Medium, 8 dedicated vCPU, 16 GB, 480 GB NVMe | Twice the cores for parallel queries and autovacuum, 4 GB shared_buffers with a 12 GB cache behind it. | Any |
| Working set 10 to 20 GB, reporting next to the API, or a database server with heavy clients | VDS Large, 16 dedicated vCPU, 32 GB, 960 GB NVMe | 8 GB shared_buffers and a 24 GB cache keep tens of millions of rows hot; 16 cores keep reporting from starving the API. | Any |
| Working set over 32 GB, more than 16 cores, or a specific disk layout (RAID, separate WAL disk) | A dedicated server | At that point a virtual plan is the wrong shape. Write to us and we will say so before you order. | Ask |
All plans on our VPS and VDS plans page run on KVM with NVMe, unmetered traffic and their own IPv4 and IPv6, and you can upgrade from the client area with a short reboot, so the cheap mistake is one size too small.
What size VDS do I need for a production database and API? Three numbers decide it
1. Working set against RAM
PostgreSQL keeps hot pages twice: in its own shared_buffers and in the operating system's file cache. If the hot part of your data fits in that combined space, reads never touch the disk and a lookup returns in well under a millisecond, as our read-only numbers show. If it does not, every miss is a disk read, multiplied by thousands of rows. Rule of thumb for a server that also runs the API: RAM of at least 1.5 times the working set, plus about 1 GB for the operating system and the API workers; our measured RAM needs of 34 self-hosted apps give you the app side.
2. Connections
Each PostgreSQL connection is a separate server process. In our lab 50 extra idle connections raised the proportional memory of all Postgres processes from 837 MB to 901 MB, about 1.3 MB each. Idle is cheap; active is not, because each active query can take work_mem per sort or hash, and more busy sessions than cores simply queue. The wiki formula above says a 4-core server is happiest with 8 to 12 busy connections; an API with 4 workers and a pool of 10 each already opens 40, which is why PgBouncer appears below.
3. Writes per second
Every commit is written to the write-ahead log and flushed to disk before PostgreSQL says "done". That flush, not CPU, caps write throughput on a small server, which is why our read-write numbers barely moved between 2 and 4 vCPUs while read-only was six to eight times higher. For a write-heavy API (events, logs, sensors) budget NVMe IOPS first and cores second; our database guide explains why commit speed is flush speed.
How we measured
One Debian 13 Incus container on our Amsterdam lab host, resized between runs with incus config set, so the same installation ran at 2 vCPU / 2 GiB (the VPS Micro shape) and at 4 vCPU / 3 GiB (standing in for VPS Mini; the lab had 3 GiB free, not 4 GB). Everything came from Debian 13's own packages: PostgreSQL 17.11, Python 3.13, FastAPI 0.115.11, uvicorn 0.32.0, asyncpg 0.30.0, wrk 4.1.0 and PgBouncer 1.24.1.
Two honest limits. A container limit does not reproduce how a KVM VPS with shared vCPUs or a VDS with dedicated vCPUs behaves; read the two sizes as "same software, more or fewer cores and RAM". And pgbench and wrk ran inside the same container as the database and the API, so the load generators ate some of the CPU they measured.
For each size we applied the documentation's starting points, 25% of RAM for shared_buffers and 75% as the planner's effective_cache_size hint, and left the rest at Debian defaults:
# as root (values for 3 GiB of RAM; at 2 GiB we used 512MB and 1536MB)
cat > /etc/postgresql/17/main/conf.d/sizing.conf <<'EOF'
shared_buffers = 768MB
effective_cache_size = 2304MB
max_connections = 100
EOF
systemctl restart postgresql@17-main
sudo -u postgres psql -Atc "show shared_buffers"
768MB
Then the datasets. One trap: pgbench -i run as postgres creates tables owned by postgres, and our first run as the bench role failed with "permission denied for table pgbench_branches" until we handed the tables over:
# as root
sudo -u postgres psql -c "CREATE ROLE bench LOGIN PASSWORD 'bench';"
sudo -u postgres createdb -O bench bench50
sudo -u postgres pgbench -i -s 50 -q bench50
for t in pgbench_accounts pgbench_branches pgbench_history pgbench_tellers; do
sudo -u postgres psql -qc "ALTER TABLE $t OWNER TO bench;" bench50
done
sudo -u postgres psql -Atc "SELECT pg_size_pretty(pg_database_size('bench50'))"
done in 8.95 s (drop tables 0.00 s, create tables 0.00 s, client-side generate 6.70 s, vacuum 0.20 s, primary keys 2.05 s).
755 MB
Scale 10 took 2.2 s and 157 MB. At 2 GB the whole 755 MB sat in RAM from the first run onwards, the "fits in RAM" case the table above assumes.
pgbench results: 2 vCPU / 2 GB against 4 vCPU / 3 GB
Each row is a 60-second run with -j equal to the vCPU count. Read-write is pgbench's default TPC-B-like transaction (five statements, one an update of a hot row); read-only is -S, one primary-key lookup per transaction, the closest thing to what an API usually does.
# as the app user (bench role); -S for read-only
PGPASSWORD=bench pgbench -h 127.0.0.1 -U bench -c 20 -j 2 -T 60 bench50
PGPASSWORD=bench pgbench -h 127.0.0.1 -U bench -S -c 20 -j 2 -T 60 bench50
latency average = 9.437 ms
tps = 2119 (without initial connection time)
latency average = 1.270 ms
tps = 15750 (without initial connection time)
| Test (scale 50, 755 MB) | 2 vCPU / 2 GB | 4 vCPU / 3 GB |
|---|---|---|
| Read-write, 10 clients | 1,799 TPS, 5.6 ms | 2,121 TPS, 4.7 ms |
| Read-write, 20 clients | 2,119 TPS, 9.4 ms | 2,304 TPS, 8.7 ms |
| Read-write, 50 clients | 1,937 TPS, 25.8 ms | 2,020 TPS, 24.8 ms |
| Read-only, 10 clients | 13,838 TPS, 0.72 ms | 14,994 TPS, 0.67 ms |
| Read-only, 20 clients | 15,750 TPS, 1.27 ms | 14,619 TPS, 1.37 ms |
| Read-only, 50 clients | 12,979 TPS, 3.85 ms | 11,563 TPS, 4.32 ms |
| Scale 10 (157 MB), read-write, 20 clients | 1,996 TPS, 10.0 ms | 1,556 TPS, 12.9 ms |
| Scale 10 (157 MB), read-only, 20 clients | 16,546 TPS, 1.21 ms | 15,329 TPS, 1.31 ms |
Read-write sat near 2,000 TPS at both sizes with 10 or 50 clients. That is the flush ceiling: more clients only made each one wait longer (5.6 ms at 10, 25.8 ms at 50) for the same total. A bigger plan buys nothing here until you change how you commit, by batching writes or, where losing the last few hundred milliseconds of transactions after a crash is acceptable, with synchronous_commit = off, which "does not create any risk of database inconsistency" (WAL settings).
Read-only peaked at 20 clients and fell at 50: more sessions than cores is a cost, not a gain. Sweet spot on a 2-core or 4-core server: 10 to 20 active connections.
And the 4 vCPU column is not better, which it should have been. While it ran, the shared lab host under our container carried a load average of 17 to 24 on 8 cores because other test instances were busy; a repeat of the 20-client read-only run afterwards gave 14,844 TPS at 1.35 ms, so the ceiling was the host, not our container. We left the numbers in because they are the most useful ones here: that is what shared CPU does to a database, and it is why the production rows in the table above say VDS.
The API load test: requests per second, p50 and p99
A production API is more than a database, so we put one in front of it: a 25-line FastAPI app on uvicorn with an asyncpg pool of 5 to 10 connections per worker, one primary-key SELECT per request, JSON back. One worker per vCPU, so the database saw 20 or 40 connections however many HTTP clients arrived.
# as the app user; one worker per vCPU
DSN=postgresql://bench:bench@127.0.0.1/bench50 python3 -m uvicorn app:app --host 127.0.0.1 --port 8000 --workers 2
curl -s http://127.0.0.1:8000/account
{"aid":2103864,"bid":22,"balance":0}
# as any user, from the same machine in our lab (from another machine in yours)
wrk -t4 -c50 -d30 --latency http://127.0.0.1:8000/account
wrk -t4 -c200 -d30 --latency http://127.0.0.1:8000/account
50% 15.12ms
99% 59.71ms
Requests/sec: 2813.04
50% 72.01ms
99% 374.62ms
Requests/sec: 2811.78
| Concurrent clients | 2 vCPU / 2 GB | 4 vCPU / 3 GB (busy host) | 4 vCPU / 3 GB (repeat) |
|---|---|---|---|
| 50 | 2,813 req/s, p50 15.1 ms, p99 59.7 ms | 1,134 req/s, p50 37.4 ms, p99 216.7 ms | 2,428 req/s, p50 15.6 ms, p99 96.4 ms |
| 200 | 2,812 req/s, p50 72.0 ms, p99 374.6 ms | 2,218 req/s, p50 73.4 ms, p99 542.3 ms | 2,385 req/s, p50 83.4 ms, p99 400 ms |
p50 is the time half of all requests beat; p99 is the time 99 of 100 beat, and the one users notice. At 50 clients the API is CPU-bound at about 15 ms median; the database part of each request is under a millisecond, the rest is Python, JSON and HTTP. Going from 50 to 200 clients added no requests per second; it only multiplied the queueing time. Once requests per second stop climbing, the fix is more cores or a second API instance, not a bigger pool.
The middle column is the busy-host run from the pgbench table: p99 at 50 clients went from 60 ms to 217 ms with nothing changed on our side. On a shared-vCPU VPS that arrives as a "the API is slow" ticket with no cause in your own logs. Our guide to a slow VPS shows how to tell steal time from your own problems.
How much RAM did Postgres plus the API use?
Measured as proportional set size (PSS), which splits shared memory fairly between the processes that map it. Plain RSS misleads: one idle backend showed 764 MB RSS because it maps the whole 768 MB of shared_buffers, while its PSS was 38 MB.
| Component | 2 vCPU / 2 GB | 4 vCPU / 3 GB |
|---|---|---|
| PostgreSQL 17, all processes (PSS, after the benchmarks) | 578 MB (512 MB shared_buffers) | 865 MB (768 MB shared_buffers) |
| FastAPI on uvicorn, all workers (PSS) | 15 MB | 15 MB |
free -m used / available | 736 MB / 1,311 MB | 1,146 MB / 1,924 MB |
| Postgres systemd cgroup, including page cache | 1,942 MB | 1,566 MB |
The last row matters most. The kernel's file cache counted against the Postgres cgroup grew to almost all of the 2 GB, because Linux uses free RAM to cache data files, and that cache is the second copy of your working set. It is not waste, and it is why "memory 95% used" is normal on a database box. Watch available in free -m and the cache hit ratio instead:
# as root; blks_hit and blks_read come from the pg_stat_database view
sudo -u postgres psql -Atc "SELECT round(100.0*sum(blks_hit)/nullif(sum(blks_hit)+sum(blks_read),0),2) AS cache_hit_pct FROM pg_stat_database"
It prints one percentage. We did not record ours, but a database that fits in RAM sits above 99% after warm-up; under 99% on a steady workload means the working set no longer fits and the next plan up is due. On the API side, "each running process has its own things, variables, and memory" (FastAPI deployment concepts): our asyncpg app was 15 MB for all workers, a Node app with a large ORM is often 100 to 200 MB per worker.
When to add PgBouncer
PgBouncer sits between your application and PostgreSQL, accepts many client connections and hands them a few real server connections in turn. You need it when your app opens far more connections than you have cores, connects per request (PHP without persistent connections, serverless functions), or shares one database with other apps. We tested the moment it matters:
# as root: 200 clients straight at Postgres (max_connections = 100), then through PgBouncer
PGPASSWORD=bench pgbench -h 127.0.0.1 -p 5432 -U bench -S -c 200 -j 4 -T 30 bench50
PGPASSWORD=bench pgbench -h 127.0.0.1 -p 6432 -U bench -S -c 200 -j 4 -T 30 bench50
pgbench: error: connection to server at "127.0.0.1", port 5432 failed: FATAL: remaining connection slots are reserved for roles with the SUPERUSER attribute
latency average = 25.290 ms
tps = 7908 (without initial connection time)
Direct, the 101st client is refused. Through PgBouncer in transaction mode with 20 server connections, all 200 were served at 7,908 TPS. At 50 clients the pooler cost a little (9,541 TPS through it against 10,691 direct, both on the busy host). The config, in /etc/pgbouncer/pgbouncer.ini:
[databases]
bench50 = host=127.0.0.1 port=5432 dbname=bench50
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
default_pool_size = 20
max_client_conn = 500
Transaction mode means your app must not rely on session state (SET, session-level prepared statements) across transactions; most ORMs, and asyncpg with statement_cache_size=0, are fine. Simple rule: an API with its own pool under about 40 connections on 4 cores does not need PgBouncer; anything beyond that, or without a pool, does.
VPS vs VDS for a database: what shared and dedicated vCPU mean
On our VPS plans a vCPU is a share of a physical core that other customers' machines also use. When they are idle you get the whole core; when they are busy, the hypervisor splits it and your database waits. On our VDS plans each vCPU is reserved for your machine alone, so a query that takes 0.7 ms at 3 in the morning takes 0.7 ms at 3 in the afternoon. A database is the worst tenant for shared CPU: it holds locks, so a delayed transaction delays the ones queued behind it, and an API in front multiplies every delay by the number of waiting requests, which is what the 217 ms p99 above looked like. Our VPS, VDS and dedicated server comparison covers other workloads; for a production database the rule is short. VPS where a slow minute costs nothing; VDS where a customer, a payment or a cron job depends on query time.
Disk follows the same logic. Every plan here is NVMe, which matters because PostgreSQL flushes on every commit, and the documentation warns that "many solid-state drives (SSD) also have volatile write-back caches" (reliability and the write-ahead log). Our VPS benchmark commands include an fio test for the random 8 kB sync writes a commit produces; run it on day one and keep the number.
When to split the database onto its own server
Everything above ran database and API on one machine, and up to VDS Small that is the right design: no network hop, one backup, one firewall. Split when the API needs more than one machine for availability or deploys, when reporting or batch jobs make the API's p99 jump, or when Postgres wants more than half the RAM. Then the database gets its own VDS Medium or Large and the API stays small. Keep both in the same city, keep port 5432 closed to the internet and connect over WireGuard; our database guide covers secure remote access to port 5432. One database and several workers is also how n8n scales; see our n8n queue mode and workers guide.
Backups: the part of sizing people forget
Servers in Prishtina include free weekly backups (the last two kept) that you restore with one click from the client area, and a daily add-on is available; servers in Amsterdam and Dublin do not include backups, so there you build your own. In either city keep a logical dump too, because a dump restores a single table and a server image does not. One line, and "pg_dump does not block other operations on the database while it is working" (SQL dump):
# as the postgres user, daily from cron or a systemd timer
pg_dump -Fc bench50 > /var/backups/bench50-$(date +%F).dump
pg_dumpall --globals-only > /var/backups/globals-$(date +%F).sql
The first file is the database in compressed custom format; the second saves roles and passwords, which pg_dump leaves out. Ship both off the server daily with our off-site backups with restic guide. For a recovery point of minutes rather than a day, add WAL archiving with pg_basebackup, a project for VDS Medium and up.
What else a production API needs on the same server
Three pieces turn the lab app into something you can leave alone for a year. A process manager: run uvicorn (or Node, or Go) as a systemd service with Restart=always, so the server is "started automatically on server startup, and without needing any human intervention", as FastAPI's notes put it. A reverse proxy with TLS: nginx or Caddy terminating HTTPS and passing to the app on a Unix socket; uvicorn's docs call nginx "recommended for additional resilience" and tell you to limit --forwarded-allow-ips to your proxy (uvicorn deployment); our reverse proxy and SSL guide compares the choices. And a firewall with only 22, 80 and 443 open, 5432 and 6432 on 127.0.0.1 as above; securing a Debian 13 VPS is the checklist. With containers, Docker on a VPS shows how to keep the database volume where backups can see it.
Frequently asked questions
What size VDS do I need to run a production database and API?
For a working set under 4 GB and up to about 1,000 requests per second, a VDS with 4 dedicated vCPUs and 8 GB RAM (our VDS Small, 240 GB NVMe) runs PostgreSQL and the API together with room to grow. In our lab even 2 to 4 vCPUs and 2 to 3 GB delivered about 2,000 read-write and 13,000 to 16,000 read-only transactions per second. Move to 8 vCPU / 16 GB when the working set passes 4 GB or writes dominate.
Should I choose VPS, VDS or dedicated hosting for a resource-intensive application?
VPS for staging and latency-tolerant workloads; VDS for production databases and APIs, because dedicated vCPUs keep response times steady when other tenants get busy; a dedicated server above 16 cores or 32 GB of working set, or for a specific disk layout. In our test the API's p99 went from 60 ms to 217 ms purely because the shared host underneath was loaded.
How much RAM does PostgreSQL need?
Enough to hold the hot part of your data in shared_buffers (the documentation suggests 25% of RAM) plus the operating system cache. A 755 MB database ran fully from memory with 2 GB total. Give the server 1.5 times the working set plus 1 GB for the OS and the application, and keep the cache hit ratio above 99%.
What is the difference between VPS and VDS for a database?
On a VPS the vCPUs are shared with other customers' machines, so query latency changes with their load; on a VDS the vCPUs are dedicated to you and latency stays flat. Both are KVM machines on NVMe. For a database that holds locks and sits behind an API, the steady latency is what you pay for.
When should I move from a VPS or VDS to a dedicated server?
When the working set no longer fits in 32 GB, when you need more than 16 cores, or when you need separate disks for data and WAL or a particular RAID layout. Until then VDS Medium (8 vCPU / 16 GB) and VDS Large (16 vCPU / 32 GB) cover almost every self-hosted application, with upgrades from the client area and a short reboot.
Do I need PgBouncer for a small API?
Not if your API keeps its own connection pool and stays under roughly 40 database connections on a 4-core server. You need it when clients connect per request, when several apps share the database, or when connections approach max_connections (100 by default). In our test 200 direct clients were refused at the 101st, while the same 200 through PgBouncer with a 20-connection pool were served at 7,908 transactions per second.
Pick the plan, then prove it with pgbench
Order the row that matches your working set and traffic on the VPS and VDS plans page, run the pgbench commands from this article in the first hour, and keep the output. If read-only latency wanders by several milliseconds between runs on a VPS, that is your sign to move to a VDS. Unsure which row you are in, or choosing between Amsterdam, Dublin and Prishtina? Send your data size, expected requests per second and framework on Telegram and we will name the plan, and say honestly if the answer is a dedicated server.