Contents
PostgreSQL 19 Beta 4 shipped on September 24 with REPACK, a new SQL command that reclaims table bloat and reorganizes storage in place, folding VACUUM FULL and CLUSTER into one command.[1] The version anyone actually wants is REPACK (CONCURRENTLY) tablename: it rewrites a bloated table without taking the table offline, something Postgres core has never done and that the community has covered for years with the pg_repack extension. Nine days after the beta shipped, a bug report on the pgsql-hackers mailing list showed the sharpest edge of that feature: run pg_dump while a REPACK CONCURRENTLY is mid-flight, and pg_dump can exit successfully having written a backup file where the target table has a COPY header and zero data rows.[2] The table has 1,000 rows. The dump has none. Nothing in the exit code says so.
What CONCURRENTLY actually does
Plain REPACK behaves like VACUUM FULL: it takes an ACCESS EXCLUSIVE lock, builds a new copy of the table, and swaps it in, blocking every reader and writer for the duration. REPACK (CONCURRENTLY) avoids that by building the new copy under a weaker SHARE UPDATE EXCLUSIVE lock, the same level VACUUM and ANALYZE already take, so normal reads and writes continue. It takes an MVCC snapshot of the table, opens a logical replication slot at that snapshot, and hands a background worker the job of decoding every insert, update, and delete that happens on the source table while the copy is being built. Those changes get replayed into the new copy. Only at the very end does it request a brief ACCESS EXCLUSIVE lock to swap the old and new relation files, and that lock is released as soon as the swap commits.[3][4] The design is based on Antonin Houska’s pg_squeeze extension, and the implementation work was presented by Álvaro Herrera at PGConf.dev this year.[5]
REPACK (CONCURRENTLY) also comes with a fixed set of restrictions: the table can’t be UNLOGGED, can’t be partitioned as a whole (individual partitions are fine), can’t be a system catalog or TOAST table, and must have either a primary key or an index usable as replica identity, since the logical decoding step needs a way to identify rows across the swap.[6][3] It also can’t run inside a transaction block, and Postgres only lets one REPACK CONCURRENTLY run at a time across the whole instance, because of how the historical snapshot for decoding gets set up. Each running instance also holds a replication slot for its duration, which matters if you’re already running Debezium or another CDC pipeline against the same database and have a fixed slot budget.[3][5]
The part that isn’t MVCC-safe
The documentation calls out one specific tradeoff by name: REPACK (CONCURRENTLY) is not MVCC-safe. In Postgres, “MVCC-safe” means a query sees a consistent view of the data as of the snapshot it started with, regardless of what other transactions commit in the meantime. Because REPACK (CONCURRENTLY) physically rewrites every row with a new xmin at swap time, a transaction that opened its snapshot before the swap, and hasn’t touched that table yet, has no way to find the rows it expects. Postgres doesn’t raise an error or fall back to the old copy. The query just returns zero rows. If that same transaction had already run any query against the table before the swap, it would hold at least an ACCESS SHARE lock, which would have blocked the swap’s ACCESS EXCLUSIVE request until the transaction finished, so the hazard is specifically for a transaction seeing that table for the first time after its snapshot was taken but before the swap.[3][6]
pg_dump runs its entire dump inside one REPEATABLE READ transaction so that every table in the output is consistent as of a single point in time. That’s exactly the shape of transaction the caveat describes: a long-lived snapshot that reaches a given table for the first time whenever pg_dump gets around to it. If a REPACK CONCURRENTLY swap on that table commits between when pg_dump opened its snapshot and when it queries that specific table, pg_dump sees an empty table, writes an empty COPY block for it, and exits normally.[2] A second bug reported the same week found that REPACK (CONCURRENTLY) can also silently lose updates made to TOASTed columns during the rewrite window, a different symptom of the same underlying issue: the rewrite and the concurrent write stream aren’t as airtight as the “concurrent” name implies yet.[7] Both reports are working their way through the mailing list ahead of general availability, expected around October.[1]
The decision this actually changes
None of this means REPACK CONCURRENTLY is broken as designed. pg_repack has carried the identical caveat for years: any command that does a full-table rewrite-and-swap in Postgres, VACUUM FULL, CLUSTER, pg_repack, or now REPACK CONCURRENTLY, can make a pre-existing snapshot see an empty table, because that’s a property of how heap visibility and xmin work, not a bug specific to one implementation.[3][8] What changes with Postgres 19 is who’s exposed to it and how often. pg_repack was a separate binary that most teams treated as a deliberate, supervised operation: install the extension, run it by hand or in a carefully reviewed job, watch it. REPACK CONCURRENTLY is a plain SQL command sitting next to VACUUM and REINDEX. It’s the kind of thing that ends up in a generic “run maintenance on tables over N gigabytes” cron job without a second thought, right alongside the nightly pg_dump that already runs on the same schedule.
If you’re evaluating REPACK CONCURRENTLY for that kind of automation, the concrete thing to check before general availability is what else touches the same tables with a long-lived snapshot: pg_dump, REPEATABLE READ or SERIALIZABLE reporting jobs, logical replication initial syncs, anything that opens a transaction and doesn’t immediately query every table it’ll eventually read. Until the pg_dump interaction gets fixed or at least documented as a real hazard rather than a theoretical one, the safer sequencing is to make sure a REPACK CONCURRENTLY run on a given table and a backup covering that table never overlap, the same discipline pg_repack runbooks already needed. It also means tables that must keep REPLICA IDENTITY FULL, or that lack a primary key, still can’t use the new command at all, so pg_repack or pg_squeeze aren’t going away with this release even for teams that adopt REPACK everywhere else.
Sources
[1] https://www.postgresql.org/about/news/postgresql-19-beta-4-released-3386/: “PostgreSQL 19 Beta 4 Released,” PostgreSQL News, September 24, 2026
[2] http://www.mail-archive.com/pgsql-hackers@lists.postgresql.org/msg240184.html: “REPACK (CONCURRENTLY) can lose data in pg_dump output,” pgsql-hackers mailing list
[3] https://www.postgresql.org/docs/19/sql-repack.html: “REPACK,” PostgreSQL 19 Documentation
[4] https://www.depesz.com/2026/04/21/waiting-for-postgresql-19-add-concurrently-option-to-repack/: “Waiting for PostgreSQL 19 – Add CONCURRENTLY option to REPACK,” select * from depesz;
[5] https://thebuild.com/blog/repack-concurrently-pgsqueeze-gets-a-promotion/: “REPACK CONCURRENTLY: pg_squeeze Gets a Promotion,” The Build
[6] https://www.postgresql.org/docs/19/mvcc-caveats.html: “13.6. Caveats,” PostgreSQL 19 Documentation
[7] http://www.mail-archive.com/pgsql-hackers@lists.postgresql.org/msg239806.html: “REPACK (CONCURRENTLY) can silently lose updates when the toast table is rewritten,” pgsql-hackers mailing list
[8] https://www.percona.com/blog/understanding-pg_repack-what-can-go-wrong-and-how-to-avoid-it/: “Understanding pg_repack: What Can Go Wrong - and How to Avoid It,” Percona