A PostgreSQL heap page can reach a state where anti-wraparound vacuum no longer needs to inspect its tuple transaction IDs. The visibility map records this state with the all-frozen bit, allowing later aggressive vacuum work to skip the page until a data change invalidates that fact.
This is separate from reclaiming dead tuples. A table with little update or delete activity can still require vacuum work because transaction IDs have a finite comparison range. Freezing converts sufficiently old tuple transaction metadata into a form that remains valid across transaction ID wraparound.
Transaction IDs have a bounded comparison window
Normal PostgreSQL transaction IDs are 32-bit values interpreted with wraparound-aware ordering. A transaction ID is treated as older or newer relative to the current position in the ID space rather than as an indefinitely increasing integer.
That representation creates a maintenance constraint for old row versions. A tuple cannot retain an ordinary inserting transaction ID forever, because after enough transaction ID consumption the circular ordering would no longer represent its age safely.
PostgreSQL prevents that state by freezing eligible tuple transaction metadata. A frozen tuple is treated as having an inserting transaction that precedes every normal transaction for visibility purposes.
Freezing does not mean that the row becomes immutable. An update still creates a new row version under normal MVCC rules. The frozen state applies to the existing tuple version and its visibility history.
Freezing and dead-tuple cleanup are different jobs
Vacuum is commonly associated with removing dead row versions, but transaction ID freezing follows a different trigger.
A heap page may contain no dead tuples and still contain live tuples with old transaction IDs. A routine vacuum focused on pages that need cleanup can skip such a page, leaving its old transaction metadata in place.
That is safe only within a bounded period. PostgreSQL eventually performs more aggressive vacuum work that visits pages which may still contain unfrozen transaction IDs, even when those pages do not need dead-tuple cleanup.
This distinction is important for stable tables. Low write volume can reduce cleanup pressure without eliminating transaction ID maintenance. Transaction IDs are consumed across the database, so an old tuple can age even while its own table receives no changes.
The visibility map carries an all-frozen bit
Each heap relation has a visibility map with two bits per heap page. One bit records all-visible state. The other records all-frozen state.
All-visible means every tuple on the page is known to be visible to all current and future transactions. That fact supports index-only scans and allows some vacuum page skipping.
All-frozen is stronger. It means every tuple on the page has transaction metadata that no future anti-wraparound vacuum needs to modify. An aggressive vacuum can therefore skip the page.
A page can be all-visible without being all-frozen. Visibility to every transaction does not by itself prove that each tuple has crossed the freezing threshold and had its transaction metadata frozen.
The two bits encode separate facts so PostgreSQL can avoid work only when the relevant condition is established.
Aggressive vacuum targets pages with remaining age debt
A regular vacuum does not always scan every heap page. Visibility-map state and cleanup requirements allow it to omit pages where no immediate work is expected.
An aggressive vacuum expands that scan boundary. It must process pages that can still contain old unfrozen XIDs or multixact IDs, including pages that are all-visible but not all-frozen.
Pages already marked all-frozen remain skippable. This makes freezing cumulative across stable regions of a table: once a page reaches all-frozen state, later anti-wraparound passes do not need to revisit it unless subsequent activity clears the state.
The table-level relfrozenxid value in pg_class records a lower bound associated with remaining unfrozen transaction IDs. Advancing that value requires PostgreSQL to account for every page that could still contain older unfrozen XIDs. Page-level all-frozen state is what permits known-safe pages to stay outside that scan.
Data changes invalidate frozen-page shortcuts
Visibility-map facts are conservative. PostgreSQL sets them only after establishing the required page state, and data-modifying operations clear relevant bits when that state can no longer be assumed.
An insert can place a new tuple with a current transaction ID on a previously frozen page. An update or delete changes tuple state and can likewise make the existing page-level assertion unusable.
The page does not become corrupt or unsafe when its all-frozen bit clears. It simply returns to the set of pages that future vacuum work may need to inspect.
This creates a practical boundary between stable and active regions. Old, unchanged pages can accumulate all-frozen state and remain cheap during later aggressive vacuums, while pages receiving ongoing writes continue to participate in transaction ID maintenance.
Freeze thresholds trade current work for future distance
PostgreSQL does not freeze every eligible-looking tuple immediately after insertion. Configuration parameters control the ages at which vacuum considers tuple freezing and table-wide aggressive scanning.
vacuum_freeze_min_age sets an age threshold below which freezing is normally avoided. Keeping very recent tuples unfrozen can avoid writing pages merely to freeze tuple metadata that may soon be updated or deleted.
vacuum_freeze_table_age influences when vacuum switches to an aggressive strategy based on the age of the table’s frozen-XID horizon. autovacuum_freeze_max_age provides the stronger anti-wraparound boundary that can trigger autovacuum even when ordinary autovacuum activity would not otherwise select the table.
These settings govern different stages of the same maintenance constraint. Lower freezing thresholds can perform more work earlier and leave a larger transaction-ID margin. Higher thresholds defer some writes but allow more age to accumulate before maintenance becomes necessary.
All-frozen coverage changes the cost of later passes
The cost of an aggressive vacuum is not determined only by table size. The fraction of heap pages already marked all-frozen changes how much heap data must be revisited.
A large mostly static table can have extensive all-frozen coverage. Aggressive vacuum can skip those pages and concentrate on regions whose transaction metadata still requires attention.
A similarly sized table with frequent modifications can repeatedly lose all-frozen state on active pages. Those pages remain candidates for future scanning even if the table has little reclaimable dead space at a particular moment.
This makes page state a more precise indicator of anti-wraparound work than raw relation size alone. The visibility map acts as a compact record of maintenance already completed.
Frozen state is a correctness boundary, not a storage optimization alone
Skipping all-frozen pages reduces I/O, but the mechanism exists around a correctness requirement: old tuple transaction IDs must not survive until circular transaction ID ordering can misclassify them.
Vacuum freezing moves tuple visibility history outside that finite normal-ID comparison window. The all-frozen bit then records that the page has no remaining tuple-level transaction ID debt for anti-wraparound processing.
The resulting behavior separates three concerns that can otherwise appear as one vacuum operation: dead-row cleanup, universal tuple visibility, and transaction ID freezing. PostgreSQL tracks them independently enough to skip work at page granularity while still preserving MVCC correctness across long database lifetimes.