PostgreSQL 19 is nearly here, and for anyone running their own database on a VPS this is the rare major release worth looking at before it’s out, not after. The fourth beta shipped on September 24, 2026,[1] and the first release candidate (RC 1) followed on October 15, 2026. A second release candidate is due alongside the November 12 update release, and general availability is planned for November 19, 2026.[5] That’s your lead time, and the window to make the upgrade boring instead of scary.

This guide sticks to what’s actually in the current 19 release notes, flags the two headline features that were pulled during beta (because planning around a reverted feature is how upgrades go sideways), and gives you a concrete pg_upgrade plan from 18 you can run the moment the GA drops.

Before you plan: two features were reverted in beta

The release notes you’ll meet at GA are not the same as the marketing around 19’s early betas. Two headline items vanished during Beta 4:[1][2]

  • Property-graph query support (SQL/PGQ) - the feature that let you traverse a graph with SQL-like syntax was reverted in Beta 4, not deferred to a point release. It is absent from the current 19 release notes.[1][2]
  • Temporal tables via FOR PORTION OF - the SQL:2011 temporal update/delete clause was also reverted in Beta 4.[1][2]

PostgreSQL remains conservative on purpose: if a feature doesn’t reach a quality bar before GA, it gets cut rather than shipped half-baked. The practical lesson for your upgrade plan is simple - don’t build new application logic against a beta-era feature. If you saw a team slide or a HN thread promising PGQ in production, that promise is off the table for 19. Build against the release notes as they stand at GA.

What’s actually new in 19 that you’ll feel on a VPS

The changes below are verified against the 19 release notes as of October 1, 2026, and picked for what matters when you’re self-hosting a single box or a small cluster.[2] The application in front of that database usually runs on the same VPS - our deploy next.js on vps guide covers that half of the stack.

REPACK replaces VACUUM FULL and CLUSTER

Disk bloat is the classic self-hosted-Postgres problem: you delete rows, the table stays fat, and eventually VACUUM FULL shows up in your Friday night. PostgreSQL 19 introduces REPACK, which unifies VACUUM FULL and CLUSTER into a single command - the old ones are retained for compatibility, but REPACK is the one to learn.[2]

The big win is the CONCURRENTLY option: REPACK ... CONCURRENTLY rebuilds the table without taking an access-exclusive lock, so reads and writes keep flowing while the repack runs.[2] On a production VPS where a table you need to compact is also one you can’t take offline at 2am, that’s the difference between a maintenance window and a non-event.

REPACK (CONCURRENTLY) orders;

There are limits worth knowing. Concurrent repack works through replication slots, and 19 adds a max_repack_replication_slots GUC to control how many it uses.[2] If you keep a lot of replication slots open, budget the headroom when you migrate settings.

Parallel autovacuum

Autovacuum is the background process that keeps your tables from dying, and in 19 it can finally parallelize the vacuuming of a table’s indexes.[2] The max is governed by the autovacuum_max_parallel_workers server variable and the per-table autovacuum_parallel_workers storage parameter.[2]

There’s a second, quieter change here: 19 adds a scoring system that prioritizes which tables autovacuum processes when it’s behind, controlled by a set of autovacuum_*_score_weight variables and visible per-table in the new pg_stat_autovacuum_scores view.[2] On a busy VPS with hundreds of tables, this matters more than raw parallelism - it stops the vacuum queue from being held hostage by one massive, rarely-accessed table.

ON CONFLICT DO SELECT ... RETURNING

The upsert that can’t tell you what it did is now a thing of the past. In 19, INSERT ... ON CONFLICT gains a DO SELECT ... RETURNING form, so a conflicting row is returned (and can optionally be locked with FOR UPDATE/FOR SHARE).[2] If you’ve written read-then-insert logic with race conditions, or a queuing table where “insert if absent, else hand me the row” keeps tripping over concurrent writers, this closes a real gap.

JIT is off by default now - read this twice

This is the migration item most people will trip on. PostgreSQL 19 disables JIT by default - previously it was on, firing based on optimizer cost estimates that the developers concluded were unreliable.[2]

For an OLTP database on a VPS, JIT off is almost always a small win (less compilation overhead per query). But if you run a lot of large analytical queries - anything heavy enough that the planner used to trigger JIT - you’ll need to turn it back on explicitly after upgrade, or your analytics queries may get slower. The relocation is one line, but it only helps if you know it’s there:

# postgresql.conf (only if you rely on JIT for analytical workloads)
jit = on

Sites doing serious analytical work in 19 must enable JIT manually; it won’t be switched on for them anymore.[2]

Breaking changes to check before you press go

The release notes’ “Migration to Version 19” section lists the incompatibilities. These four are the ones most likely to bite a self-hosted setup:[2]

  1. RADIUS authentication is removed. PostgreSQL only ever supported RADIUS over UDP, which maintains can’t secure, so it’s gone. If pg_hba.conf references RADIUS, that path will break - you’ll need LDAP, OAuth, or the built-in methods instead.[2]
  2. standard_conforming_strings is now always on. It’s no longer a setting you toggle. Critically, dumps produced by pre-19 pg_dump/pg_dumpall with this setting off will not load cleanly into 19. When you dump an old cluster for restore, use a 19-era pg_dump (or set standard_conforming_strings = on), or you’ll inherit subtle string-literal misbehavior.[2]
  3. New-look inet/cidr indexes. The default opclass for these types changes from btree_gist to GiST, because the btree_gist inet/cidr opclasses are broken - they can exclude rows that should be returned. So pg_upgrade will disallow upgrading any cluster that has a btree_gist index on inet or cidr columns. If you fit this, drop or rebuild those indexes before the upgrade or it’ll hard-stop.[2]
  4. max_locks_per_transaction default doubles from 64 to 128, and lock size allocation changed - meaning existing lock settings now need to be roughly doubled to hold the same capacity. Also, carriage returns and line feeds are banned in database/role/tablespace names (and pg_upgrade refuses clusters that use them), and the MULE_INTERNAL encoding is gone (affected databases need a dump/restore with a different encoding).[2]

Two of these - the btree_gist inet/cidr blocker and the standard_conforming_strings dump rule - are the ones to grep for today, because fixing them is the kind of thing better done before GA week than during it.

The upgrade plan: 18 → 19

Major-version upgrades are never in-place. The 19 documentation lists three supported paths - pg_dumpall, pg_upgrade, or logical replication.[3] For a VPS, pg_upgrade in --link mode is almost always the right call: it upgrades in minutes instead of dumping and restoring your whole dataset, and it works while keeping downtime tiny.[3]

Here’s the sequence I’d run when the GA lands. Do not skip the backup.

1. Pre-flight checks

# gold: the current version and layout
postgres --version
psql -c 'SHOW data_directory;'

# find btree_gist inet/cidr indexes BEFORE upgrading (pg_upgrade will refuse them)
psql -d mydb -c "
SELECT c.relname AS index_name, i.indrelid::regclass AS table_name, opc.opcname
FROM pg_index i
JOIN pg_class c ON c.oid = i.indexrelid
JOIN pg_opclass opc ON opc.oid = ANY(i.indclass::oid[])
WHERE opc.opcname IN ('gist_inet_ops', 'gist_cidr_ops');
"

btree_gist names its inet/cidr operator classes gist_inet_ops and gist_cidr_ops (the core GiST class is inet_ops), so that query lists exactly the indexes pg_upgrade will refuse to carry over.[2][4] Run it in every database of the cluster. Recreate those indexes with the core inet_ops opclass (or drop them) before you even start the migration - a plain REINDEX keeps the old opclass and doesn’t help.

2. Take a real backup

pg_upgrade --link is fast because it hard-links the data files - which means it mutates your data directory. You need a recoverable copy before it runs:

pg_dumpall > /backup/pre19_$(date +%F).sql

Use the backup as insurance, not as your primary migration mechanism. A full dump/restore through pg_dumpall is the second supported path if you’d rather be slow and simple, and it’s the older, battle-tested default when pg_upgrade hits an edge case.[3]

3. Install PG19 binaries, stop the old server

Install PostgreSQL 19 from your distro’s repo (the package manager’s scripts handle the cluster layout - check their docs for major-upgrade helpers). Then stop the running 18 server:

sudo systemctl stop postgresql@18-main   # exact unit differs by distro

4. Run pg_upgrade

With both old and new binaries present, run the new pg_upgrade. The --link flag is what makes it fast:

# as the postgres user; paths are distro-dependent
sudo -u postgres /usr/lib/postgresql/19/bin/pg_upgrade \
  --old-bindir /usr/lib/postgresql/18/bin \
  --new-bindir /usr/lib/postgresql/19/bin \
  --old-datadir /var/lib/postgresql/18/main \
  --new-datadir /var/lib/postgresql/19/main \
  --link --check        # dry-run first

Run it with --check first - a dry run that only checks compatibility and changes nothing - fix anything it flags (at minimum the btree_gist inet/cidr indexes), then rerun the same command without --check for real. The full pg_upgrade procedure, including the checks it performs, is documented in the upgrade guide.[3]

5. Verify, then re-enable fire (systemd wants a restart)

After upgrade, the documented systemd example in 19 now includes a restart setting - worth copying into your unit so the service survives daemon restarts cleanly.[2] Then:

sudo systemctl start postgresql@19-main
sudo -u postgres psql -c 'SELECT version();'   # should say 19.x

For logical-replication users, 19 also replicates sequence values, and logical replication can be enabled without a server restart when wal_level is replica - relevant if you’re sequencing a rolling migration to avoid a hard cutover.[2]

What to do while you have lead time

The honest takeaway: the disruptive parts of 18→19 are the removals and defaults, not the new features. You have a month or more to de-risk it. Concretely:

  • Find and rebuild any btree_gist inet/cidr indexes now - pg_upgrade will refuse the whole cluster otherwise, and this is a one-time prep task with no downtime risk if you build the replacement concurrently.
  • Plan a dump test with a 19-era pg_dump if you’ll upgrade via restore, so the standard_conforming_strings behavior is already proven before cutover.
  • Decide your JIT stance based on your real workload - enable it in postgresql.conf during migration if you’re analytical-heavy.
  • Check pg_hba.conf for RADIUS and have the replacement auth method ready.
  • Test on a scratch cluster - spin up 19 in parallel, load a copy, run your read path. The upgrade doc specifically recommends testing client applications against the new version before switching over.[3]

If you skip the planning and just run pg_upgrade on GA Thursday, the most likely failure is the btree_gist inet/cidr hard-stop - purely because it’s the least-visible incompatibility. Grep for it now, and the rest of this release is mostly upside: a locking-free REPACK, an autovacuum that stops being the thing that wakes you at 3am, and an upsert that finally tells you the truth.

Sources

  1. PostgreSQL Global Development Group - “PostgreSQL 19 Beta 4 Released!” (Beta 4, posted 2026-09-24; RC planned early October, GA may follow in October; lists features reverted in Beta 4 incl. SQL/PGQ and FOR PORTION OF). https://www.postgresql.org/about/news/postgresql-19-beta-4-released-3386/ - checked 2026-10-01
  2. PostgreSQL 19 Documentation - “E.1. Release 19” release notes (REPACK, parallel autovacuum, ON CONFLICT DO SELECT ... RETURNING, JIT disabled by default, RADIUS removal, standard_conforming_strings forced on, btree_gist inet/cidr pg_upgrade block, max_locks_per_transaction default, MULE_INTERNAL removal, systemd restart example; as of 2026-09-14). https://www.postgresql.org/docs/19/release-19.html - checked 2026-10-01
  3. PostgreSQL 19 Documentation - “18.6. Upgrading a PostgreSQL Cluster” (pg_upgrade / pg_dumpall / logical replication upgrade methods; testing client apps on the new version). https://www.postgresql.org/docs/19/upgrading.html - checked 2026-10-01
  4. PostgreSQL 19 Documentation - “F.8. btree_gist” (§F.8.2: gist_inet_ops / gist_cidr_ops unreliable; 19 makes core inet_ops the default for inet/cidr). https://www.postgresql.org/docs/19/btree-gist.html - checked 2026-10-03
  5. PostgreSQL Global Development Group - “PostgreSQL 19 RC 1 Released!” (posted 2026-10-15; RC 2 on the 2026-11-12 update-release day; GA planned 2026-11-19). https://www.postgresql.org/about/news/postgresql-19-rc-1-released-3409/ - checked 2026-10-17