PostgreSQL 19 is in beta — Beta 1 landed 4 June 2026, Beta 2 on 16 July 2026, and the general availability date is whatever the project announces. This is the cheapest window you will get for migration work: the incompatible changes are settled enough to audit against, and none of your production traffic depends on them yet. What follows sorts the eight breaking changes by how they will hurt you — the two that make pg_upgrade refuse outright, the three that silently change behavior, and the three that need configuration rethought — then covers the features worth upgrading for and a beta test plan you can actually run.
What beta is and is not for
The rule is simple: do not put production data on a beta. Beta exists so you can find out where your code breaks, not so you can use new features early.
Three things are worth doing now. Run your full test suite against beta using production schema and anonymized data, which finds more than any number of readings of the release notes. Audit the breaking changes below against your actual cluster. And run the pg_upgrade preflight, because two of these changes cause a hard refusal and you want that news months in advance rather than during a maintenance window.
The release notes are explicit that migrating data from any previous release requires a dump/restore using pg_dumpall, or pg_upgrade, or logical replication. There is no in-place path.
The eight breaking changes
Two that make pg_upgrade refuse
Default index opclasses for inet and cidr move from btree_gist to GiST. The stated reason is blunt: the btree_gist inet/cidr opclasses are broken because they can exclude rows that should be returned. If you have such an index today, some of your query results have probably been wrong. pg_upgrade will refuse to upgrade a cluster containing them.
Go check this now rather than at upgrade time. Tables with network address columns and a GiST index are where to look.
Carriage returns and line feeds are disallowed in database, role, and tablespace names. This was changed to avoid security problems, and pg_upgrade refuses clusters using such names. It sounds impossible until you remember that automation which builds names by string concatenation can absolutely capture a trailing newline. One query settles it.
Three that silently change behavior
These do not error. They just behave differently, which makes them the dangerous category.
JIT is disabled by default. It was previously enabled and activated based on optimizer costs; the project determined that costing to be unreliable and flipped the default. Sites running many large analytical queries must now enable JIT manually. If your reporting queries depended on it, they get slower after the upgrade with nothing in the logs to explain why.
Default TOAST compression changes from pglz to lz4, via default_toast_compression. New large values compress with lz4; existing data keeps whatever it has. lz4 trades some compression ratio for considerably more speed, which is usually a net win — but if your storage headroom is thin, the ratio change belongs in your capacity plan before the upgrade, not after.
max_locks_per_transaction default goes from 64 to 128. The important part is the reason: lock size allocation changed, so existing settings must be doubled to match their previous capacity. If you explicitly tuned this — and anyone with many partitioned tables has — carrying the old number forward halves your effective lock capacity, which surfaces later as unexplained "out of shared memory" failures.
Three that need replanning
standard_conforming_strings is forced to always be on in the server. The consequence lands on old dumps: files produced by pre-19 pg_dump/pg_dumpall with standard_conforming_strings = off will not properly load into PostgreSQL 19 and later. The documented remedy is to create dumps using version 19 or later of those tools, or to ensure the setting is on when dumping.
Teams with archived dumps for disaster recovery should sit with that one. Some of those files may no longer be usable for restoring onto a current server, which makes it a restore-drill item rather than a footnote.
RADIUS support is removed, because PostgreSQL only supported RADIUS over UDP, which is unfixably insecure. Anyone using the radius authentication method has to move first. That is an authentication architecture change, not a config tweak, so give it real lead time.
MULE_INTERNAL encoding is removed. Databases using it must be dumped and restored with a different encoding. Few clusters are affected, but the ones that are face a full data migration.
Choosing an upgrade path
| Path | Downtime | Fits |
|---|---|---|
pg_dumpall plus restore |
Long, scales with data size | Small databases; or when encoding or collation must change |
pg_upgrade |
Short | The default for large databases |
| Logical replication | Shortest, reversible | Tight windows, if you can absorb the dual-running complexity |
pg_upgrade answers most cases, but the two hard-refusal checks have to clear first. Run it with --check in a non-production environment; it reports blockers without any downtime, so there is no reason to defer that step.
Logical replication earns its complexity through reversibility: both versions run, and a problem means switching back. The costs are sequence handling, DDL not replicating, and load during initial sync. PostgreSQL 19 adds sequence support in logical replication, which lowers that barrier.
If you take that route, resumability deserves explicit design: where replication picks up after an interruption, what happens when a position is lost, and how downstream consumers stay idempotent under redelivery. Those questions are the same across data stores — we worked through resume token invalidation and replay boundaries in MongoDB change streams resumability, and the "make the position recoverable first, then make the consumer idempotent" ordering transfers directly to replication slots.
Features worth upgrading for
REPACK unifies VACUUM FULL and CLUSTER. The two commands did similar things under confusing names, so 19 unifies them as REPACK, retaining the old ones for compatibility. The substantive addition is CONCURRENTLY: rebuilding a table without access-exclusive locking, supported by a new max_repack_replication_slots setting.
This addresses a long-standing operational trap. Reclaiming space from a bloated table used to mean locking it for the duration, which meant a maintenance window, which meant it kept getting postponed. If you have a table everyone knows is bloated and nobody dares touch, this alone may justify the upgrade.
Parallel autovacuum. The worker ceiling is set by autovacuum_max_parallel_workers, with a per-table autovacuum_parallel_workers storage parameter. On write-heavy large tables, autovacuum falling behind is a familiar nightmare: cleanup cannot keep pace with garbage generation and bloat compounds. Parallelism targets that directly.
pg_plan_advice helps stabilize and control planner decisions. When a plan regresses and you simply want the old one back, the previous options were toggling enable_* parameters or rewriting the query to trick the planner.
Also shipping: INSERT ... ON CONFLICT DO SELECT ... RETURNING, which returns conflicting rows and can lock them with FOR UPDATE/FOR SHARE; GROUP BY ALL, grouping automatically by every non-aggregate, non-window target list entry; COPY TO in JSON format, with FORCE_ARRAY for a single JSON array; FOR PORTION OF on UPDATE and DELETE for temporal ranges; IGNORE NULLS/RESPECT NULLS on lead(), lag(), first_value(), last_value(), and nth_value(); and SQL/PGQ property graph queries.
A beta test plan
- Check the blockers first. Query for btree_gist
inet/cidrindexes, and for database, role, or tablespace names containing carriage returns or line feeds. Everything else is wasted effort until these clear. - Stand up a beta environment with production schema and anonymized data — not production data.
- Get
pg_upgrade --checkclean, resolving every item it reports. - Run the full test suite, paying attention to string literal escaping, anything that depended on JIT for performance, and paths involving many partitions.
- Diff query plans. Analytical query plans and timings shift once JIT is off. Measure before deciding whether to re-enable it.
- Exercise backup and restore. Regenerate a dump with the 19 tooling and restore it, confirming your archival strategy still holds on the new major version.
- Load test lock-heavy paths. If
max_locks_per_transactionwas explicitly tuned, double it per the guidance and then measure. - Write down rollback criteria — which metric regression aborts the upgrade — rather than deciding in the moment.
Steps 4 through 7 are worth automating rather than performing once. More betas will ship before GA, and re-running the suite against each one is where the value is. For scheduled jobs with dependencies and failure notification, a scheduler beats accumulating cron entries; Cronova is our take on that — a single self-hosted binary, DAGs in YAML, embedded SQLite, a web console and REST API, with retries and alerting in the scheduling layer.
The methodology generalizes beyond Postgres. We wrote up the same staged approach for a language runtime in the Python 3.15 beta migration guide: find hard blockers, then silent behavior changes, and only then evaluate new features.
Failure modes
Copying the old configuration forward. max_locks_per_transaction is the canonical example — the number is unchanged but its meaning is not, so carrying it over halves capacity. Review settings individually rather than porting the file.
Forgetting JIT is off. Analytical queries slow down and the team investigates indexes, statistics, and hardware before anyone suspects a changed default. Record baseline timings for key queries as the first post-upgrade step.
Old dumps that will not restore. Archives created years ago with standard_conforming_strings = off no longer load into 19. A backup's value is in restoring, not in existing.
Accumulating real data on the beta. Catalog changes between beta and GA can force a rebuild. Design the beta environment to be disposable.
Testing features but not operations. Autovacuum, locking, and compression behavior all changed. None of that shows up in functional tests; it shows up under production load.
Migration checklist
- No btree_gist
inet/cidrindexes remain, or a rebuild is planned - No database, role, or tablespace names contain carriage returns or line feeds
pg_upgrade --checkpasses in a non-production environment with every reported item resolved- Any explicit
max_locks_per_transactionhas been recalculated on the doubling guidance - Analytical query timings with JIT off are measured, and re-enabling it is a decision based on that data
- Archived dumps have been restore-tested; regenerated with 19 tooling where needed
- Connections using the
radiusauthentication method have moved to another method - Databases using MULE_INTERNAL encoding have an encoding migration planned
- Application string literal escaping has been reviewed and does not rely on
standard_conforming_strings = off - Capacity planning accounts for the TOAST compression ratio change from pglz to lz4
- Rollback criteria are written down and the chosen upgrade path actually supports that rollback
FAQ
When does PostgreSQL 19 reach GA? The project ships a major release roughly annually; the date is whatever is officially announced. Beta 1 was released 4 June 2026 and Beta 2 on 16 July 2026.
Can I upgrade in place? No. The documentation requires a dump/restore with pg_dumpall, pg_upgrade, or logical replication.
Why was JIT disabled? JIT was previously activated based on optimizer costing, and that costing was determined to be unreliable. The capability is intact; sites that want it enable it explicitly.
Will existing data be recompressed as lz4? No. default_toast_compression affects newly written values; existing TOAST data keeps its current compression.
Does REPACK replace VACUUM FULL? REPACK is the unified command, with VACUUM FULL and CLUSTER retained for compatibility. Write new code against REPACK, which also offers CONCURRENTLY that the older commands lack.
Why is pg_upgrade refusing my cluster? Most likely one of the two blockers: btree_gist inet/cidr indexes, or newlines in database, role, or tablespace names. Run --check for the specific report.