PostgreSQL 19 Ships Three Features That Look Like Free Wins. Each Has a Catch.
Atomic get-or-create, online table repacking, and parallel autovacuum all sound like pure upside. Here's the configuration trap hiding behind each one.
PostgreSQL 19 bundles three features that rarely get discussed together
PostgreSQL 19 Beta 1 shipped in June 2026. The project’s own roadmap points to a September 2026 general release, the usual once-a-year cadence the Postgres team has kept for over a decade. Most of the beta coverage has treated the release as a features list: SQL/PGQ property graph queries, faster foreign key checks, a new query-plan-hints mechanism, a new REPACK command, parallel autovacuum. Each of those has had its own explainer written in isolation, and most of them are genuinely interesting to read on their own.
What’s missing is the version a team actually needs before an upgrade. Which changes take effect the moment you flip a switch, and which one will quietly misconfigure itself if nobody does the arithmetic first? Three features fit that second description: ON CONFLICT DO SELECT, REPACK CONCURRENTLY, and parallel autovacuum. Each fixes something that has been a genuine operational headache for years. Each also has exactly one trap that shows up only once it’s running against a table with real production size and real production concurrency, not the beta blog post’s 200-row example.
None of the three are exotic. Every team running Postgres past a certain size has already built a workaround for the problem each one solves: a hand-rolled upsert lock, a scheduled pg_repack run coordinated around a maintenance window, a manually tuned autovacuum threshold on the one table that always falls behind. Postgres 19 doesn’t just add new syntax. It quietly asks whether those workarounds are still earning their complexity once the built-in version exists, and that’s the question this article is actually about.
It’s also worth naming which features are getting the conference talks and which are getting this article. SQL/PGQ and query plan hints are the ones people will demo on stage, because graph queries and hint syntax are visibly new capability. ON CONFLICT DO SELECT, REPACK CONCURRENTLY, and parallel autovacuum are not new capability in that sense. Postgres could already do get-or-create, table rewrites, and vacuum tuning; these three just make the existing behaviour atomic, online, and parallel. That’s exactly why they’re the ones likely to get paged for at 3am if a team upgrades without checking the fine print, and the ones with the least discussion of what actually goes wrong.
ON CONFLICT DO SELECT ends the lookup-then-insert race, but RETURNING is not optional
Atomic get-or-create is one of those problems most backend engineers get wrong at least once. The obvious approach is to SELECT and check whether a row exists, then INSERT if it doesn’t. That races against any other transaction doing the same lookup at the same moment. The usual Postgres workaround has been INSERT … ON CONFLICT DO UPDATE with a no-op update, or a SELECT … FOR UPDATE wrapped in an advisory lock. Both work. Neither reads like what the code is actually trying to do, and both leave a comment in the codebase explaining why the query looks stranger than the problem it solves.
ON CONFLICT DO SELECT is Postgres 19’s direct answer: insert if the row is new, return the existing row untouched if it’s a duplicate, and skip the second round-trip either way.
-- Plain get-or-create: insert, or hand back the existing row
INSERT INTO envelopes (external_id, source, payload)
VALUES ($1, $2, $3)
ON CONFLICT (external_id, source) DO SELECT
RETURNING *;
-- Need to check-and-modify the row you just found, not just read it?
INSERT INTO envelopes (external_id, source, payload)
VALUES ($1, $2, $3)
ON CONFLICT (external_id, source) DO SELECT FOR UPDATE
RETURNING *;Two details catch people who skim the release notes instead of testing against real code. RETURNING is mandatory on DO SELECT. Leave it off and Postgres rejects the statement at parse time, which is a kinder failure than most Postgres surprises but still one worth catching in CI rather than in a deploy. And unlike DO NOTHING, DO SELECT requires an explicit conflict target: you name the constraint or column set, not just "on any conflict." That’s stricter on purpose. A silent get-or-create with no target is exactly the kind of statement that misbehaves quietly after a schema migration nobody double-checked.
Cybertec's write-up on the feature and Neon's PostgreSQL 19 guide both cover the syntax in more depth than the release notes do.
We run a version of this exact pattern in FlowVerify’s envelope-creation endpoint today, except it’s still the old workaround: a SELECT inside a transaction guarded by an advisory lock on the external ID, written before the atomic version existed. It collapses to one statement instead of four in Postgres 19, and it removes an entire class of duplicate-envelope report that only ever showed up under retry storms.
REPACK CONCURRENTLY replaces VACUUM FULL’s outage, if you can pay for it in disk
VACUUM FULL and CLUSTER have always done the same basic thing: rewrite a bloated table into a fresh, compact copy. And they’ve always done it by holding an exclusive lock for the entire operation, which is why "run VACUUM FULL on the 400GB table" has been a maintenance-window sentence for two decades, not a Tuesday-afternoon sentence. Teams that can’t tolerate that lock have spent years running the third-party pg_repack extension instead, accepting the operational risk of a C extension inside the database process as the price of staying online.
REPACK, Postgres 19’s new command, folds VACUUM FULL and CLUSTER into one interface and adds a CONCURRENTLY option that changes the trade entirely. Instead of locking the table for the rewrite, it copies a snapshot, uses logical decoding to replay every change made during the copy, and takes only a short exclusive lock at the very end to swap the rewritten table in. The table stays readable and writable for essentially the whole operation, and the extension is no longer required.
The catch is in what "copies a snapshot" actually costs. REPACK CONCURRENTLY still builds a complete second copy of the table before the swap. That is the same disk cost VACUUM FULL always had, minus the lock. On a multi-hundred-gigabyte table that’s not a rounding error. It’s the same headroom check teams have long run before using pg_repack, the extension this command is built to make unnecessary.
The Build's coverage of the feature also flags the new max_repack_replication_slots setting, which defaults to 5. REPACK CONCURRENTLY consumes a logical replication slot for the run’s duration, so a cluster already using logical replication for CDC or cross-region sync could be closer to that ceiling than expected.
Parallel autovacuum only helps wide, heavily-indexed tables, and it changes your memory math
Autovacuum’s biggest practical complaint has never been that it’s too aggressive. It’s that a single worker can get stuck on one wide table with a dozen indexes for hours, starving every other table in the cluster of maintenance in the meantime. Parallel autovacuum targets exactly that scenario, and only that scenario: the parallelism applies to index cleanup, not to the heap scan that finds dead tuples or the heap truncation at the end, both of which stay single-threaded. A narrow table with two indexes will see basically no change. A wide table with ten or twelve will finish in a fraction of the time, because index cleanup was the part of the vacuum that scaled linearly with index count in the first place.
The trap is memory, not CPU. Each parallel worker gets its own allocation of maintenance_work_mem, multiplied by however many autovacuum workers are running concurrently across the cluster. The rough ceiling is autovacuum_max_workers times autovacuum_max_parallel_workers times maintenance_work_mem. Take a cluster already tuned for heavy maintenance: maintenance_work_mem at 2GB, the default autovacuum_max_workers of 3. Simply raising autovacuum_max_parallel_workers to 4 to speed up cleanup on one wide table pushes the worst-case ceiling to 24GB, before anything else on the box asks for RAM. That’s arithmetic worth redoing, not skipping, before filing the upgrade ticket. A cluster with less headroom than that can hit swap or get OOM-killed during exactly the maintenance window that was supposed to make things safer.
Parallel autovacuum doesn’t turn itself on. It ships with a conservative default that keeps it effectively inactive until autovacuum_max_parallel_workers is deliberately raised, so nothing changes in behaviour purely from upgrading the binary. The risk shows up only once someone reaches for that knob to fix a slow vacuum, without rechecking the memory ceiling that comes with it.
Three fixes, three traps, side by side
Lined up together, the pattern is consistent. Each feature removes a workaround that has existed for years, and each asks a team to spend a different resource to get it: a stricter syntax requirement, disk space, or memory headroom.
| Feature | What it fixes | The trap |
|---|---|---|
| ON CONFLICT DO SELECT | Get-or-create races between SELECT and INSERT | RETURNING and a conflict target are mandatory, not optional |
| REPACK CONCURRENTLY | VACUUM FULL's exclusive-lock outage | Needs a full second copy of the table on disk, plus a replication slot |
| Parallel autovacuum | One wide table starving every other table's maintenance | Memory scales with workers × parallel workers × maintenance_work_mem |
What to actually test before GA
Before scheduling a PostgreSQL 19 upgrade for any cluster past a few dozen gigabytes, four checks are worth running against a staging copy, not the release notes:
- Audit every ON CONFLICT DO UPDATE that’s really a get-or-create in disguise, and check whether RETURNING is already part of the calling code. DO SELECT needs it, and older ORM-generated queries often don’t ask for it.
- Measure free disk space against the size of the three or four largest tables. REPACK CONCURRENTLY needs room for a full copy of each one it touches, on top of whatever WAL and logical-slot storage the run consumes.
- Recompute the autovacuum memory ceiling, autovacuum_max_workers times autovacuum_max_parallel_workers times maintenance_work_mem, against whatever is already tuned in postgresql.conf, before touching autovacuum_max_parallel_workers.
- Count logical replication slots already in use. REPACK CONCURRENTLY needs its own, capped by max_repack_replication_slots (default 5), and a cluster already running logical replication for other purposes could be closer to that ceiling than expected.
The disk check is the one teams skip, because it feels like arithmetic anyone can do in their head. In practice it’s worth running for real against the actual table, not an estimate:
-- Current on-disk size of the table you're about to repack
SELECT pg_size_pretty(pg_total_relation_size('envelopes')) AS current_size;
-- Free space on the volume Postgres's data directory lives on
-- (run this at the shell, not in psql)
-- df -h $(psql -tAc "SHOW data_directory;")If the free space on that volume is smaller than the table’s current size plus a safety margin for WAL growth during the run, REPACK CONCURRENTLY is not ready to run against that table yet, no matter how good the lock story sounds. Postgres 19 also exposes progress views for the new command, consistent with the pattern it already uses for CREATE INDEX CONCURRENTLY and plain VACUUM, so a long-running repack against a large table doesn’t have to be a black box while it’s in flight.
The workarounds outlive their reason if nobody retires them
None of these three features change what Postgres is for. They close gaps that have had workarounds, extensions, advisory locks, maintenance windows, for so long that the workarounds started to look like the only way to do it. The more interesting test for PostgreSQL 19 isn’t whether SQL/PGQ graph queries or query plan hints get adopted quickly. It’s whether teams actually retire pg_repack and hand-rolled upsert locking once the built-in version ships, or keep running them out of habit long after the reason for them is gone.
Frequently asked questions
Related reading
Reddit's zero-downtime migration of 500 Kafka brokers wasn't about Kafka. It was three reusable techniques.
Reddit moved 500+ Kafka brokers and a petabyte of live data from EC2 to Kubernetes with zero downtime. The three techniques behind it aren't specific to Kafka.
CRDTs vs OT is a solved question in 2026. Where you draw the sync boundary is not.
Local-first sync in 2026 isn't a CRDT-library decision anymore. It's a boundary decision: row-level, document, or event log, and each one fails differently once you ship it.
Railway disconnected a carrier to contain an outage. It cut its last route instead.
A July 2 Railway outage report shows the real damage came after the fix: a containment decision that removed the last default route, and a well-known Linux default that turned a fallback path into a silent bottleneck.