Upgrade PostgreSQL to a New Major Version for Django
Upgrade the PostgreSQL behind a Django app to a new major version: check Django and psycopg support, choose dump and restore, pg_upgrade, or logical replication, then analyze, verify, and roll back safely.
Problem statement
Every PostgreSQL major version is supported for five years. Running an unsupported version means no security fixes, and new Django releases drop support for old PostgreSQL versions, so a delayed database upgrade eventually blocks your Django upgrades too.
Major upgrades change the on-disk format, so you cannot just install new packages and restart. You need a method that fits your database size and downtime tolerance, checks that Django and psycopg support the target, and a rollback path. This guide compares the three standard methods, walks through them for a Django app, and lists what to do after the switch.
- Check compatibility: your Django version's supported PostgreSQL range, your psycopg version, and extensions (PostGIS, pg_trgm) available for the target.
- Rehearse on a copy of production and time it.
- Small databases or generous windows:
pg_dumpthenpg_restoreinto the new version. - Larger databases with a short window:
pg_upgrade --link(on Debian and Ubuntu,pg_upgradecluster). - Minimal downtime: logical replication from old to new, then switch Django's connection.
- After the switch: run
vacuumdb --all --analyze-in-stages, update extensions, run Django checks and smoke tests, and keep the old cluster until you are confident.
Step-by-step solution
1. Check compatibility
- Django: each release documents supported PostgreSQL versions; older PostgreSQL versions are dropped as they approach end of life. Check the "Databases" notes for your Django version and the one you will upgrade to next.
- psycopg: psycopg 3 and psycopg2 both work with current PostgreSQL versions; upgrade the driver if it is very old.
- Extensions: install the target version's packages for every extension you use (
postgresql-17-postgis-3alongsidepostgresql-17, for example). - Managed services: check which target versions and upgrade paths the provider supports.
List what the database uses:
SELECT extname, extversion FROM pg_extension;
SELECT version();
2. Rehearse on a copy
Restore a recent backup to a scratch server running the old version, perform the upgrade there, point a staging Django instance at the result, and run your test suite and smoke tests. Record the duration: it is your downtime estimate.
3. Method A: dump and restore
Simple and reliable, and it also rebuilds indexes and removes bloat. Downtime grows with database size.
# stop writes: maintenance mode, stop Gunicorn and Celery
pg_dump --format=custom --no-owner --dbname="postgresql://myapp@old-db:5432/myapp" --file=myapp.dump
pg_restore --jobs=4 --no-owner --role=myapp --dbname="postgresql://postgres@new-db:5432/myapp" myapp.dump
- Use the new version's
pg_dumpbinary where possible; newer clients can dump older servers. --jobsparallelises the restore across tables and indexes.- Create roles and extensions on the new server first (
pg_dumpall --roles-onlyexports roles).
4. Method B: pg_upgrade
pg_upgrade converts the cluster's catalog to the new version and reuses the data files. With --link, files are hard-linked instead of copied, so even large databases upgrade in minutes.
On Debian and Ubuntu, the pg_upgradecluster wrapper handles configuration and ports:
sudo apt install postgresql-17 postgresql-17-postgis-3 # plus extension packages you need
sudo systemctl stop gunicorn celery-worker celery-beat
sudo pg_dropcluster 17 main --stop # remove the empty default cluster of the new version
sudo pg_upgradecluster -m upgrade --link 16 main
sudo pg_lsclusters
The upgraded cluster takes over the old port; the old cluster is moved aside and stopped. With plain pg_upgrade, run pg_upgrade --check first, which reports incompatibilities without changing anything.
Rollback caveat: with --link, once the new cluster has been started, the old cluster can no longer be used safely, because they share data files. Take a backup (or a volume snapshot) immediately before upgrading. Without --link, files are copied, which needs double the disk space but keeps the old cluster intact.
5. Method C: logical replication
For near-zero downtime, replicate from the old server to a new one, then switch:
-- on the old server (wal_level = logical)
CREATE PUBLICATION myapp_pub FOR ALL TABLES;
-- on the new server, after creating the schema with pg_dump --schema-only
CREATE SUBSCRIPTION myapp_sub
CONNECTION 'host=old-db dbname=myapp user=replicator password=...'
PUBLICATION myapp_pub;
When the subscription has caught up, stop writes briefly, confirm it is in sync, update sequences on the new server (logical replication does not copy sequence values), switch Django's DATABASES to the new server, and resume. Tables need primary keys (or a replica identity) for updates and deletes to replicate, which Django models have by default. Schema changes are not replicated, so freeze migrations during the process.
Sequence values can be synchronised with a query per sequence, for example:
SELECT setval('orders_order_id_seq', (SELECT max(id) FROM orders_order));
6. Managed services
- Amazon RDS: in-place major version upgrade (downtime while it runs; take a snapshot first), or RDS Blue/Green Deployments, which use logical replication for a short switchover.
- Cloud SQL, Azure Database for PostgreSQL: in-place major version upgrades from the console or CLI, with a backup taken automatically or recommended beforehand.
Always test the same upgrade on a restored copy of the production instance first.
7. After the switch
sudo -u postgres vacuumdb --all --analyze-in-stages
pg_upgrade does not carry planner statistics over; until ANALYZE runs, query plans can be poor. --analyze-in-stages produces usable statistics quickly, then refines them.
Then:
ALTER EXTENSION pg_trgm UPDATE;
SELECT postgis_extensions_upgrade(); -- if you use PostGIS
python manage.py check --deploy
python manage.py showmigrations | grep "\[ \]" || echo "no unapplied migrations"
Run smoke tests covering logins, writes, search, and background tasks, and watch error rates and slow queries for the next hours.
If the operating system was also upgraded (a new glibc), text indexes using libc collations may need REINDEX, because collation sort order can change between glibc versions. PostgreSQL logs collation version warnings when it detects this.
Verification check:
SELECT version();
SELECT count(*) FROM django_migrations;
SELECT relname, last_analyze, last_autoanalyze FROM pg_stat_user_tables ORDER BY n_live_tup DESC LIMIT 5;
Explanation
Major PostgreSQL versions change internal catalogs and sometimes storage details, so data must be migrated rather than reused as-is. Dump and restore rewrites everything logically; pg_upgrade rewrites only the catalog and keeps the data files; logical replication streams row changes into a new cluster while the old one keeps serving. For Django, the database version is mostly invisible, as long as the ORM's supported range includes it, extensions are present, and statistics are refreshed so query plans stay good.
Edge cases / notes
Connection poolers and replicas
Update PgBouncer's target and restart it after switching servers. Physical replicas must be rebuilt for the new major version; they cannot replicate across major versions.
Rollback
With dump and restore or logical replication, the old server stays intact, so rollback is pointing Django back to it (losing writes made on the new one unless you replicate back). With pg_upgrade --link, rollback means restoring the pre-upgrade backup or snapshot.
When to automate this
Script the rehearsal: restore the latest backup to a scratch server, upgrade, run Django's tests and smoke tests against it, and report the timing. Run it before every major upgrade and whenever your database size has grown significantly.
Next steps
- Coordinate with Django upgrades using the Django Version Upgrade Checklist for Production.
- Make sure backups are restorable first with Backup and Restore PostgreSQL for Django Apps.
- Review settings after the upgrade with the PostgreSQL Production Checklist for Django.
- For spatial databases, see Deploy GeoDjango with PostGIS in Production.
FAQ
Which PostgreSQL versions does Django support?
Each Django release supports a range of PostgreSQL versions listed in its documentation; old versions are dropped as they approach end of life. Check before upgrading either component.
What is the fastest way to upgrade PostgreSQL?
pg_upgrade with --link usually completes in minutes regardless of data size, because data files are linked rather than copied.
Can I upgrade PostgreSQL without downtime for Django?
Nearly, with logical replication: replicate to the new version, stop writes briefly, sync sequences, and switch the connection.
Why are queries slow right after a PostgreSQL upgrade?
pg_upgrade does not preserve planner statistics. Run vacuumdb --all --analyze-in-stages immediately after the upgrade.