PostgreSQL 19 ships on November 19. The headline feature for most production teams isn’t a new query planner — it’s REPACK CONCURRENTLY, a built-in command that eliminates the maintenance window for table bloat cleanup. If you’ve ever watched VACUUM FULL hold your production table hostage for ten minutes, or hit the pg_repack wall on a managed service, the wait is almost over.
Why Table Bloat Is Your Problem (Even If You Haven’t Looked)
PostgreSQL’s MVCC model keeps old row versions on disk when you run an UPDATE. The old version — a “dead tuple” — stays there until VACUUM reclaims it. Under normal conditions, autovacuum handles this. Under write-heavy conditions, it doesn’t keep up.
The default autovacuum_scale_factor is 0.2 — autovacuum only fires after 20% of a table is dead. On a 100 GB table, that’s 20 GB of garbage before cleanup even starts. By then, sequential scans are slower, your shared buffer cache is polluted, and indexes have grown. Real production cases show tables at 40x their actual data size.
When bloat gets bad enough, there are three options: schedule a VACUUM FULL during a maintenance window, install pg_repack, or accept the bloat. None of these is great. PostgreSQL 19 adds a fourth option that beats all three.
REPACK CONCURRENTLY: What Changes
VACUUM FULL holds an ACCESS EXCLUSIVE lock for the entire table rewrite — reads and writes blocked, nothing gets through. REPACK CONCURRENTLY does the same full rewrite but holds that lock only briefly, at the final file swap step. The difference in production impact is significant.
The numbers from independent testing tell the story:
| Method | Max client wait | Writes during rewrite | Extension required | Works on managed Postgres |
|---|---|---|---|---|
| VACUUM FULL | 969 ms | 33 | No | Yes |
| pg_repack | ~125 ms | Yes | Yes | Usually no |
| REPACK CONCURRENTLY | 125 ms | 3,988 | No | Yes (PG19+) |
VACUUM FULL completes in under a second total — but your clients wait almost a full second and almost nothing writes through. REPACK CONCURRENTLY takes about four times longer overall, but your application keeps running. For a production table serving traffic around the clock, that tradeoff is obvious.
The managed-service angle matters. pg_repack requires superuser access to install. On AWS RDS, you get rds_superuser, not true superuser — pg_repack is blocked. Supabase users who install it via the dashboard still hit “You must be a superuser to use pg_repack.” Neon’s documentation on PG19 REPACK confirms the built-in approach works without superuser. When your managed provider upgrades to PG19, it’s just there — no support ticket required.
How to Use REPACK CONCURRENTLY
First, find your worst offenders:
SELECT relname,
n_live_tup,
n_dead_tup,
round(n_dead_tup::numeric / nullif(n_live_tup + n_dead_tup, 0) * 100, 1) AS dead_pct
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000
ORDER BY n_dead_tup DESC;
Then run the repack:
-- Online repack, table stays writable
REPACK (CONCURRENTLY) orders;
-- Online repack + reorder by index (replaces CLUSTER, now online)
REPACK (CONCURRENTLY) orders USING INDEX orders_created_at_idx;
-- Include fresh statistics
REPACK (CONCURRENTLY, ANALYZE) orders;
Prerequisites: the table needs an indexable primary key (or index-based replica identity), wal_level must be replica or higher (the default since PG10), and you can’t run this inside a transaction block.
The limits worth knowing: partitioned tables can’t be repacked as a whole (repack individual partitions instead), only one concurrent REPACK runs per database at a time, and there’s a subtle MVCC caveat — a pg_dump or long-running open transaction during REPACK CONCURRENTLY may see inconsistent data at the logical replication boundary. Don’t schedule a REPACK while a dump is running.
Bonus: ON CONFLICT DO SELECT Closes the Get-or-Create Race
PostgreSQL 19 also adds a third action to ON CONFLICT. The classic get-or-create pattern has always had a race condition:
-- PG18 and earlier: two round-trips, race possible under concurrent load
INSERT INTO users (email) VALUES ($1) ON CONFLICT (email) DO NOTHING;
SELECT * FROM users WHERE email = $1;
PostgreSQL 19 fixes this in one statement:
-- PG19: atomic, one statement, no race
INSERT INTO users (email, name)
VALUES ('user@example.com', 'Alice')
ON CONFLICT (email) DO SELECT
RETURNING *;
When a conflict is detected, the existing row comes back through RETURNING — no second round-trip, no gap for a concurrent transaction to slip through. Add FOR UPDATE to lock the row atomically in the same statement. It’s a small syntax addition with a surprisingly large impact on correctness in high-concurrency apps.
What to Do Before November 19
RC2 ships November 12. GA is November 19. Upgrading to PG19 requires a major version upgrade — either pg_upgrade or a pg_dump/pg_restore cycle. This is not an in-place minor update.
The practical plan: run the bloat check query above and find out what you’re working with. Test PG19 in staging once GA drops. Schedule the production upgrade for early 2027 after the first point release (19.1) ships and the community has shaken out any surprises. The RC1 upgrade notes are worth reviewing now — the breaking changes list is short but includes a few gotchas for specific extension users. The official PostgreSQL 19 release notes have the complete list.
pg_repack was a good workaround for 15 years. PostgreSQL 19 makes it obsolete. REPACK CONCURRENTLY should be your default going forward — VACUUM FULL is for edge cases only.

