Skip to content

    Back to articles

    5 min read

    PostgreSQL’s concurrent REPACK moves the maintenance risk

    Online does not mean cost-free

    A table rewrite has always asked me to choose which pain I prefer: block access while reclaiming space, or add operational machinery to avoid most of that block. PostgreSQL 19’s new REPACK command makes that choice more interesting. Its CONCURRENTLY option is intended to rewrite a table without access-exclusive locking, while the ordinary command retains the simpler, more disruptive path. PostgreSQL describes REPACK as rewriting table contents to reclaim storage occupied by dead tuples, unlike ordinary VACUUM, which does not rewrite the table into a new file. (PostgreSQL 19 REPACK documentation)

    That matters because a maintenance window is not the only cost of a rewrite. Avoiding a long block on reads and writes is valuable, but a concurrent operation still competes for resources and creates work that the rest of the system must absorb. I would treat “online” as a statement about availability during much of the operation—not a guarantee that production load, replication, storage, or recovery remain unaffected.

    PostgreSQL 19 Beta 4 is a feature preview, and the project warns that details may change before general availability. I would evaluate this as a capability to test, not a reason to change a production maintenance plan before the final behavior and provider support are clear. (PostgreSQL 19 Beta 4 announcement)

    Reconsider the dependency, not just the command

    The new command changes the dependency question. Teams that use the pg_repack extension to perform online rewrites may be able to reduce their reliance on that extension once they can run PostgreSQL 19. The project describes pg_repack as a tool for reorganizing tables with minimal locks; PostgreSQL’s release notes describe the new REPACK as combining the functions of VACUUM FULL and CLUSTER, with concurrent rewriting as an option. (pg_repack project, PostgreSQL 19 release notes)

    That does not make every existing extension use obsolete. I would compare the actual feature set, supported versions, provider constraints, and operational procedures before removing a dependency. A built-in command can simplify installation and compatibility management, but the migration still needs a tested invocation, monitoring, permissions, and a response plan if the operation cannot finish as expected. Nor does the feature make scheduled maintenance disappear: it gives me another way to carry out a rewrite when a blocking window is difficult to arrange.

    For a database that already has a tolerable maintenance window, I would not automatically choose the concurrent path. The ordinary rewrite may be easier to schedule and reason about if its lock is acceptable. For a busy table where blocking access would be the dominant risk, I would test the concurrent option and ask whether the risks it introduces are manageable. That is a system-specific decision, not a universal upgrade rule.

    Follow the work into writes, disk and replicas

    Concurrent rewriting changes when users encounter the operation; it does not make the work vanish. If applications continue to write to the target table while the rewrite runs, I would test at representative write rates and watch for increased contention, slower transactions, or a longer operation. I would not infer safety from a quiet test database: the important question is how the rewrite behaves alongside the real mix of reads, writes, and maintenance tasks.

    I would also budget for the rewrite as a period of overlapping storage use. The old table has to remain available while a replacement is built, so free space that looks adequate for the steady state may not be adequate during maintenance. I would check the space available where the table and its indexes live, include headroom for normal growth, and define a stop condition before starting. If the available space is tight, I would rather use a planned window or defer the operation than gamble on reaching the final swap.

    Replicas and recovery deserve the same attention. PostgreSQL 19 adds max_repack_replication_slots in connection with concurrent repacking, which is a reminder that the feature has replication-related operational settings, not just a SQL command. (PostgreSQL 19 release notes) Before adopting it, I would test the effect on write-ahead log generation, replica lag, and retained WAL in the topology I actually run. Those are measurements to make in a representative environment, not outcomes I would assume from the word “concurrent.” I would verify that replicas catch up within acceptable limits and that the system has enough storage if they do not.

    Make the decision reversible

    My evaluation would start with a small, representative table and a written runbook: prerequisites, command, monitoring signals, stop conditions, and cleanup or recovery steps. I would test normal completion, interruption, a replica falling behind, and a restart after a failed attempt. If a test cannot establish what happens to the table and any temporary state after interruption, I would not call the process operationally ready.

    Then I would compare the result with the current plan. Does the concurrent path remove a maintenance window that is genuinely hard to get, or merely replace a scheduled interruption with a longer period of elevated resource use? Does it let me retire an extension I would otherwise need to install and support, or does my environment impose separate constraints? Can I absorb the extra storage and replication work without threatening normal operations?

    I would adopt concurrent REPACK where avoiding the blocking rewrite is worth the added resource and recovery planning. I would keep a scheduled window where that is the simpler, safer trade. PostgreSQL has moved the maintenance trade-off; it has not removed the need to make one.

    Drafted with AI assistance, reviewed and edited by Ali.