Plain PostgreSQL VACUUM normally leaves reclaimed heap space inside the relation for later reuse. A distinct tail-truncation phase can instead shorten the relation file when a contiguous run of empty pages exists at its physical end. That phase requires an ACCESS EXCLUSIVE lock.
The lock boundary makes tail truncation materially different from ordinary vacuum cleanup. Routine heap and index maintenance is designed to coexist with normal reads and writes, while shortening the physical relation requires a brief period in which concurrent table access cannot proceed.
Reusable space and returned space are different outcomes
Removing dead tuple versions does not normally reduce a table file immediately. Vacuum can mark space on heap pages as available, and later inserts or updates can reuse that capacity.
This internal reuse avoids rewriting the relation and is sufficient for many workloads. A table can therefore contain substantial reusable space while its file size remains unchanged.
Returning space to the operating system has a stricter physical condition. Empty pages must form a contiguous suffix at the end of the relation. Free pages in the middle cannot simply be removed because later block numbers would shift.
The physical position of free space therefore matters as much as its quantity.
Tail truncation changes the relation boundary
A heap relation is addressed as numbered blocks. Removing blocks from its end changes the valid upper boundary of that block range.
PostgreSQL protects that boundary change with ACCESS EXCLUSIVE. This lock conflicts with every table-level lock mode, including the ACCESS SHARE lock used by ordinary reads.
The truncation operation is consequently not equivalent to marking individual pages reusable. Reusable pages remain part of the relation and retain their block numbers. Tail truncation removes blocks from the relation’s physical extent.
Empty pages must reach the physical end
Consider a relation with free blocks scattered among live blocks:
live | free | live | free | live | freeThe final free block alone may be removable if it is wholly empty and eligible, but earlier free blocks cannot be cut away while live blocks follow them.
A different layout creates a larger removable suffix:
live | live | free | free | free | freeHere the contiguous empty tail can potentially be truncated as one range.
This constraint means a large delete does not guarantee a corresponding file-size reduction. If surviving tuples occupy pages near the end, the relation has no long empty suffix to remove even when many earlier pages contain free space.
The lock can become visible on busy tables
An ACCESS EXCLUSIVE request conflicts with active readers and writers. On a heavily accessed relation, acquiring that lock can therefore be harder than performing ordinary vacuum work.
The practical effect depends on concurrency and transaction duration. Short transactions can leave frequent acquisition windows. Long-running operations can make the lock opportunity less predictable.
Once held, the lock also conflicts with new table access until the truncation work releases it. The truncation phase is intended to be bounded rather than to turn the whole plain vacuum into an exclusively locked operation, but its presence can still matter for latency-sensitive workloads.
This behavior is separate from VACUUM FULL. VACUUM FULL rewrites the relation and requires ACCESS EXCLUSIVE for its operation; plain vacuum tail truncation is a narrower mechanism attached to ordinary vacuum processing.
Truncation can be disabled independently
PostgreSQL exposes tail truncation as a separate vacuum policy. The server setting vacuum_truncate controls the default behavior, and a table can override it with its storage parameter.
A vacuum command can also specify the TRUNCATE option explicitly:
VACUUM (TRUNCATE FALSE) events;Disabling truncation does not disable dead-tuple cleanup. Vacuum can still reclaim tuple space for reuse inside the table without attempting to shorten the relation tail.
A persistent table-level setting can separate this choice from individual commands:
ALTER TABLE events SET (vacuum_truncate = false);That tradeoff preserves internal free-space reuse while avoiding the exclusive-lock requirement associated with returning tail pages to the operating system.
File size can stay high after successful cleanup
A stable relation size after vacuum is not, by itself, evidence that vacuum failed to reclaim dead space.
Several states can produce the same visible file size. Free capacity may be distributed through the heap rather than concentrated at the tail. A live page may anchor the relation’s final block range. Tail truncation may be disabled. Concurrency may also prevent a truncation opportunity from being used during a particular run.
Database-level space accounting and operating-system file size therefore describe different aspects of storage. The former can reflect capacity available for reuse even when the latter does not fall.
Workload shape determines the value of truncation
Tables with recurring growth followed by deletion from the newest physical region can form empty tails naturally. Tail truncation can return that space without rewriting the entire table.
Workloads that delete rows across the full key space tend to create holes throughout the relation instead. Those holes remain useful for future tuple placement but do not form a removable suffix.
Physical tuple placement also changes over time through updates, page reuse, bulk loads, and maintenance. Logical key order alone does not guarantee that deleted rows correspond to the physical end of the heap.
Tail truncation is therefore a boundary optimization rather than a general compaction mechanism. It converts a specific layout of already-empty terminal pages into a smaller relation file, with an exclusive-lock requirement at the point where that physical boundary changes.