Why re-evaluate RDS in 2026
Amazon RDS for PostgreSQL is a solid managed database, but its billing model is designed so the invoice scales non-linearly with growth. Each component is billed separately: instance compute, gp3 storage per GB, provisioned IOPS, Multi-AZ replication nodes, backups beyond the included window, outbound data transfers. A db.r6g.4xlarge instance in Multi-AZ with 500 GB of gp3 storage exceeds $3,000/month on us-east-1 on-demand pricing. The same workload on a three-node Hetzner Cloud cluster runs around $835/month — a saving of $2,315/month, roughly $27,800 over twelve months (source: selfhost.dev, May 2026, HN item #48816129). This figure covers a complete high-availability cluster. For a development database, a staging environment, or a business workload without replication, a single node is enough for a fraction of that cost. The pricing landscape also shifted in 2026: Hetzner revised its rates in April then June 2026, and OVHcloud updated its VPS pricing in April 2026. These increases change the absolute numbers, not the logic — self-hosting remains structurally cheaper once the database exceeds a few dozen GB and the traffic demands a non-burstable instance. The question is no longer whether self-hosting costs less — the numbers confirm it — but whether the operational overhead is worth the savings.
What you gain by switching to a VPS
- Fixed, predictable cost — no billing per GB, extra IOPS, or simultaneous connections; you size your VPS once and the price does not change with query volume.
- Unrestricted extensions — PostGIS, TimescaleDB, pg_partman, pgvector, citus: no arbitrary restrictions, unlike RDS where the list is closed and unsupported extensions are absent.
- Root access and fine-grained configuration —
postgresql.conf,pg_hba.conf,huge_pages,wal_level, connection poolers: adjust every parameter without going through a cloud console and without restrictions on system tools. - Version control — you decide when to move from PostgreSQL 16 to 17, without imposed maintenance windows or forced deprecation by the vendor.
- Data within your perimeter — choose the datacenter, encrypt volumes according to your internal policy, no cross-region data transfer charges on every analytical read.
- Full portability —
pg_dumpor logical replication moves your data to any other host without vendor friction and without egress fees. - Administration option available — if operational overhead remains the main objection, the VPS administration option covers routine tasks (updates, monitoring, backups); you keep control, not the workload.
PostgreSQL on VPS vs Amazon RDS: comparison table
| Criterion | PostgreSQL on VPS | Amazon RDS for PostgreSQL |
|---|---|---|
| Monthly cost (standard workload) | Fixed — proportional to the chosen server | Variable — compute + storage + IOPS + Multi-AZ + network egress |
| High-availability example (3 nodes / Multi-AZ) | ~$835/month (Hetzner CCX53 cluster, May 2026) | ~$3,150/month (db.r6g.4xlarge Multi-AZ, us-east-1) |
| PostgreSQL extensions | All, including PostGIS, pgvector, TimescaleDB, citus | Restricted list; unsupported extensions absent |
| Root / OS access | Yes — OS choice, Docker, cron, kernel tuning, pgBouncer | No — AWS API only, restricted parameters |
| Version upgrades | At your own pace, without imposed maintenance windows | Scheduled or forced by AWS at deprecation |
| Operational overhead | Backups, updates, monitoring to manage | Automated by AWS: backups, patches, failover |
| Portability | Full — `pg_dump` or logical replication, no egress fee | Tied to AWS ecosystem, data export is paid |
Prerequisites before migrating
A VPS with 4 GB RAM and 2 vCPU is sufficient for most web application databases under 50 GB with moderate traffic and fewer than 50 concurrent connections. For a more demanding database, an analytical schema with many joins, or a service exposed to traffic spikes, plan for at least 8 GB RAM. SSD NVMe storage is essential: PostgreSQL's random access patterns on a rotational disk or entry-level SATA SSD significantly degrade performance, particularly on cache warm-up after a restart. Root access is required — standard on any cloud VPS. On the network side, you will need a fixed IPv4 address or an internal domain name to point your application connections to. Finally, plan a migration window where both databases run in parallel: the application points to RDS while you populate the VPS, verify consistency, then switch the connection variable. Duration depends on volume: a 5 GB dump restored in parallel takes minutes; 200 GB may take several dozen.
Install and configure PostgreSQL on a VPS
Install PostgreSQL from the official PGDG repository
On Ubuntu 24.04 or Debian 12, add the PostgreSQL Global Development Group repository to get the current version rather than the distribution-packaged one: curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg then echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list. Then: sudo apt update && sudo apt install -y postgresql-17.
Create a dedicated user and database
Connect as postgres: sudo -u postgres psql. Then create a dedicated application user and isolated database: CREATE USER myapp WITH PASSWORD 'strong-password'; followed by CREATE DATABASE myapp OWNER myapp;. Exit with \q. Never use the postgres role in your application connection string — a bug or injection would operate with superuser rights.
Configure postgresql.conf for expected load
The configuration file is at /etc/postgresql/17/main/postgresql.conf. Key parameters for a 4 GB VPS: shared_buffers = 1GB (25% of RAM), effective_cache_size = 3GB, work_mem = 16MB, maintenance_work_mem = 256MB, max_connections = 100. Restart after any change: sudo systemctl restart postgresql. For an 8 GB VPS, raise shared_buffers to 2 GB and effective_cache_size to 6 GB.
Restrict network connections in pg_hba.conf
By default, PostgreSQL only listens on localhost. If your application is on the same server, leave it as is. For an application on another machine, edit /etc/postgresql/17/main/pg_hba.conf and add: host myapp myapp <app-IP>/32 scram-sha-256. In postgresql.conf, set listen_addresses = 'localhost,<VPS-IP>'. Reload: sudo systemctl reload postgresql.
Export from RDS and import on the VPS
From a machine with access to both hosts, export using the custom format: pg_dump -h <rds-endpoint> -U <user> -Fc <database> -f dump.pgc. Then import on the VPS in parallel across 4 workers: pg_restore -h localhost -U myapp -d myapp -j 4 dump.pgc. Verify row counts on a few critical tables before switching the application connection.
Automate backups with a cron job
Create /usr/local/bin/pg-backup.sh: PGPASSWORD='password' pg_dump -U myapp myapp -Fc > /var/backups/pg/myapp-$(date +%Y%m%d-%H%M).pgc. Make it executable: chmod +x /usr/local/bin/pg-backup.sh. Add the crontab entry: 0 3 * * * /usr/local/bin/pg-backup.sh. Keep dumps encrypted on external storage or transfer them via rsync to a second VPS for an off-site copy.
Enable monitoring with pg_stat_statements
In postgresql.conf, add shared_preload_libraries = 'pg_stat_statements'. Restart PostgreSQL, then activate the extension in your database: CREATE EXTENSION pg_stat_statements;. Identify slow queries: SELECT query, mean_exec_time, calls FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;. This replaces RDS Performance Insights for daily diagnosis.
Post-install hardening: disable the system postgres account for SSH connections (sudo passwd -l postgres); enable ssl = on in postgresql.conf with a Let's Encrypt or self-signed certificate to encrypt network connections between the application and the database; enable ufw and open port 5432 only to known application IPs (sudo ufw allow from <app-IP> to any port 5432). These three steps cover the essential attack surface of a database exposed on a non-private network.
Troubleshooting: common errors after migration
The errors below appear frequently in the first hours after an RDS-to-VPS migration. Each has a precise message and a targeted remedy — read the full message before acting, as several distinct causes share the same error code.
Five common errors and their solutions
FATAL: password authentication failed for user "myapp"— the password sent does not match the one stored, or the authentication method differs (md5vsscram-sha-256). Check the corresponding line inpg_hba.confand reload:sudo systemctl reload postgresql. If you changed the password from psql, make sure the application connection string reflects the new one.FATAL: no pg_hba.conf entry for host "<IP>", user "myapp", database "myapp", SSL off— the source IP of the connection is not authorised inpg_hba.conf. Add the missing entry for that IP and reload. If you are not using SSL, make sure the line useshostand nothostssl.ERROR: extension "uuid-ossp" does not exist(or any extension absent on RDS) — the extension is available in PostgreSQL but not enabled in this database. Run from psql:CREATE EXTENSION IF NOT EXISTS "uuid-ossp";. If the extension is absent from the package, install it:sudo apt install postgresql-17-<extension>.FATAL: remaining connection slots are reserved for non-replication superuser connections—max_connectionsis reached. Increase it inpostgresql.confand restart, or installpgBouncerto pool connections: a pool of 10 real connections can serve hundreds of application clients.pg_restore: error: could not execute query: ERROR: role "rdsadmin" does not exist— RDS creates internal roles absent from any non-AWS PostgreSQL. Add--no-owner --no-privilegestopg_restore:pg_restore --no-owner --no-privileges -h localhost -U myapp -d myapp dump.pgc. Objects are imported without attempting to reassign ownership to RDS roles.
What this comparison does not cover — and what to know before deciding
Self-hosting transfers to your team the management of backups, security updates and monitoring. This is a reality, not an argument against it — evaluate it against the cost saved. For a solo developer or a small team without operational expertise, the VPS administration option covers routine tasks — updates, monitoring, verified backups — without you having to orchestrate them. RDS remains relevant in two specific situations: when multi-region resilience is a contractual requirement and you lack the resources to implement it manually, and when consumption-based billing genuinely suits a very small, rarely-accessed database (a VPS runs and is billed 24/7 even at zero load). For everything else — mid-sized database, stable workload, team with basic system skills — the numbers clearly favour self-hosting. The articles <a href="/blog/heberger-postgresql-vps">PostgreSQL on VPS: installation and best practices</a> and <a href="/blog/postgresql-fin-de-vie-planifier-montee-version">PostgreSQL end of life: planning the version upgrade</a> complement this guide on the operational and major version management side.