From 36666e87fe05ed24e413d1cd2f512bfacd65be38 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 22 Apr 2023 13:04:13 -0700
Subject: [PATCH v4 9/9] Overhaul freezing and wraparound docs.

This is almost a complete rewrite.  "Preventing Transaction ID
Wraparound Failures" becomes "Freezing to manage the transaction ID
space".  This is follow-up work to commit 1de58df4, which added
page-level freezing to VACUUM.

The emphasis is now on the physical work of freezing pages.  This flows
a little better than it otherwise would due to recent structural
cleanups to maintenance.sgml; discussion about freezing now immediately
follows discussion of cleanup of dead tuples.  We still talk about the
problem of the system activating xidStopLimit protections in the same
section, but we use much less alarmist language about data corruption,
and are no longer overly concerned about the very worst case.  We don't
rescind the recommendation that users recover from an xidStopLimit
outage by using single user mode, though that seems like something we
should aim to do in the near future.

There is no longer a separate sect3 to discuss MultiXactId related
issues.  VACUUM now performs exactly the same processing steps when it
freezes a page, independent of the trigger condition.

Also move recommendation about setting autovacuum_freeze_min_age
reloption in append-only tables that was originally added by the
autovacuum_vacuum_insert_scale_factor commit over to "Routine
Vacuuming", where it now appears in the form of a "Tip" box.

Also describe the page-level freezing FPI optimization added by commit
1de58df4.  This is expected to trigger the majority of all freezing with
many types of workloads.

Also move "table age" monitoring query to monitoring.sgml, though leave
behind a couple of forwarding links in maintenance.sgml's discussion of
freezing and relfrozenxid advancement.
---
 doc/src/sgml/catalogs.sgml                |   18 +-
 doc/src/sgml/config.sgml                  |   79 +-
 doc/src/sgml/logicaldecoding.sgml         |    4 +-
 doc/src/sgml/maintenance.sgml             | 1062 ++++++++++++++++-----
 doc/src/sgml/monitoring.sgml              |   80 +-
 doc/src/sgml/ref/create_table.sgml        |    9 +-
 doc/src/sgml/ref/prepare_transaction.sgml |   14 +-
 doc/src/sgml/ref/vacuum.sgml              |   14 +-
 doc/src/sgml/ref/vacuumdb.sgml            |   17 +-
 doc/src/sgml/storage.sgml                 |    3 +-
 doc/src/sgml/xact.sgml                    |    2 +-
 11 files changed, 963 insertions(+), 339 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 524084055..7bb123e32 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2243,8 +2243,9 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <para>
        All transaction IDs before this one have been replaced with a permanent
        (<quote>frozen</quote>) transaction ID in this table.  This is used to track
-       whether the table needs to be vacuumed in order to prevent transaction
-       ID wraparound or to allow <literal>pg_xact</literal> to be shrunk.  Zero
+       whether the table needs an aggressive <command>VACUUM</command> (see
+       <xref linkend="vacuum-aggressive"/>) or an anti-wraparound autovacuum
+       (see <xref linkend="vacuum-antiwraparound-autovacuums"/>).  Zero
        (<symbol>InvalidTransactionId</symbol>) if the relation is not a table.
       </para></entry>
      </row>
@@ -2256,8 +2257,9 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <para>
        All multixact IDs before this one have been replaced by a
        transaction ID in this table.  This is used to track
-       whether the table needs to be vacuumed in order to prevent multixact ID
-       wraparound or to allow <literal>pg_multixact</literal> to be shrunk.  Zero
+       whether the table needs an aggressive <command>VACUUM</command> (see
+       <xref linkend="vacuum-aggressive"/>) or an anti-wraparound autovacuum
+       (see <xref linkend="vacuum-antiwraparound-autovacuums"/>).  Zero
        (<symbol>InvalidMultiXactId</symbol>) if the relation is not a table.
       </para></entry>
      </row>
@@ -3053,8 +3055,8 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <para>
        All transaction IDs before this one have been replaced with a permanent
        (<quote>frozen</quote>) transaction ID in this database.  This is used to
-       track whether the database needs to be vacuumed in order to prevent
-       transaction ID wraparound or to allow <literal>pg_xact</literal> to be shrunk.
+       track whether the database allows <literal>pg_xact</literal> to be
+       shrunk (see <xref linkend="vacuum-truncate-xact-status"/>).
        It is the minimum of the per-table
        <link linkend="catalog-pg-class"><structname>pg_class</structname></link>.<structfield>relfrozenxid</structfield> values.
       </para></entry>
@@ -3067,8 +3069,8 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <para>
        All multixact IDs before this one have been replaced with a
        transaction ID in this database.  This is used to
-       track whether the database needs to be vacuumed in order to prevent
-       multixact ID wraparound or to allow <literal>pg_multixact</literal> to be shrunk.
+       track whether the database allows <literal>pg_multixact</literal> to be
+       shrunk (see <xref linkend="vacuum-truncate-xact-status"/>).
        It is the minimum of the per-table
        <link linkend="catalog-pg-class"><structname>pg_class</structname></link>.<structfield>relminmxid</structfield> values.
       </para></entry>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 909a3f28c..0a94ff46a 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2812,7 +2812,9 @@ include_dir 'conf.d'
          <literal>1min</literal>) are only allowed because they may sometimes be
          useful for testing.  While a setting as high as <literal>60d</literal> is
          allowed, please note that in many workloads extreme bloat or
-         transaction ID wraparound may occur in much shorter time frames.
+         transaction ID exhaustion may occur in much shorter time frames
+         (see <xref linkend="vacuum-aggressive"/> and
+         <xref linkend="vacuum-xid-exhaustion"/>).
         </para>
 
         <para>
@@ -8358,9 +8360,8 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
        </para>
        <para>
         Note that even when this parameter is disabled, the system
-        will launch autovacuum processes if necessary to
-        prevent transaction ID wraparound.  See <xref
-        linkend="vacuum-for-wraparound"/> for more information.
+        will launch anti-wraparound autovacuums.  See <xref
+        linkend="vacuum-antiwraparound-autovacuums"/> for more information.
        </para>
       </listitem>
      </varlistentry>
@@ -8536,20 +8537,17 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
        <para>
         Specifies the maximum age (in transactions) that a table's
         <structname>pg_class</structname>.<structfield>relfrozenxid</structfield> field can
-        attain before a <command>VACUUM</command> operation is forced
-        to prevent transaction ID wraparound within the table.
-        Note that the system will launch autovacuum processes to
-        prevent wraparound even when autovacuum is otherwise disabled.
+        attain before an anti-wraparound autovacuum is forced for the table.
+        Note that the system will launch anti-wraparound autovacuum
+        processes even when autovacuum is otherwise disabled.
        </para>
 
        <para>
-        Vacuum also allows removal of old files from the
-        <filename>pg_xact</filename> subdirectory, which is why the default
-        is a relatively low 200 million transactions.
+        The default is 200 million transactions.
         This parameter can only be set at server start, but the setting
         can be reduced for individual tables by
         changing table storage parameters.
-        For more information see <xref linkend="vacuum-for-wraparound"/>.
+        For more information see <xref linkend="vacuum-antiwraparound-autovacuums"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -8565,20 +8563,16 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
        <para>
         Specifies the maximum age (in multixacts) that a table's
         <structname>pg_class</structname>.<structfield>relminmxid</structfield> field can
-        attain before a <command>VACUUM</command> operation is forced to
-        prevent multixact ID wraparound within the table.
-        Note that the system will launch autovacuum processes to
-        prevent wraparound even when autovacuum is otherwise disabled.
+        attain before an anti-wraparound autovacuum is forced for the table.
+        Note that the system will launch anti-wraparound autovacuum
+        processes even when autovacuum is otherwise disabled.
        </para>
 
        <para>
-        Vacuuming multixacts also allows removal of old files from the
-        <filename>pg_multixact/members</filename> and <filename>pg_multixact/offsets</filename>
-        subdirectories, which is why the default is a relatively low
-        400 million multixacts.
+        The default is 400 million Multixact IDs.
         This parameter can only be set at server start, but the setting can
         be reduced for individual tables by changing table storage parameters.
-        For more information see <xref linkend="vacuum-for-multixact-wraparound"/>.
+        For more information see <xref linkend="vacuum-antiwraparound-autovacuums"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9282,10 +9276,11 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         set this value anywhere from zero to two billion, <command>VACUUM</command>
         will silently limit the effective value to 95% of
         <xref linkend="guc-autovacuum-freeze-max-age"/>, so that a
-        periodic manual <command>VACUUM</command> has a chance to run before an
-        anti-wraparound autovacuum is launched for the table. For more
-        information see
-        <xref linkend="vacuum-for-wraparound"/>.
+        standard autovacuum (or a manual <command>VACUUM</command>) has a
+        chance to run using <command>VACUUM</command>'s aggressive strategy
+        before an anti-wraparound autovacuum is launched for the table.  For
+        more information see <xref linkend="vacuum-aggressive"/> and <xref
+         linkend="vacuum-antiwraparound-autovacuums"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9307,7 +9302,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         the value of <xref linkend="guc-autovacuum-freeze-max-age"/>, so
         that there is not an unreasonably short time between forced
         autovacuums.  For more information see <xref
-        linkend="vacuum-for-wraparound"/>.
+        linkend="vacuum-freezing-xid-space"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9324,9 +9319,11 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         <structname>pg_class</structname>.<structfield>relfrozenxid</structfield>
         field can attain before <command>VACUUM</command> takes
         extraordinary measures to avoid system-wide transaction ID
-        wraparound failure.  This is <command>VACUUM</command>'s
-        strategy of last resort.  The failsafe typically triggers
-        when an autovacuum to prevent transaction ID wraparound has
+        exhaustion (see <xref linkend="vacuum-xid-exhaustion"/>).
+        This is <command>VACUUM</command>'s strategy of last resort.  The
+        failsafe typically triggers when an
+        <link linkend="vacuum-antiwraparound-autovacuums">anti-wraparound
+         autovacuum</link> has
         already been running for some time, though it's possible for
         the failsafe to trigger during any <command>VACUUM</command>.
        </para>
@@ -9344,7 +9341,8 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         set this value anywhere from zero to 2.1 billion,
         <command>VACUUM</command> will silently adjust the effective
         value to no less than 105% of <xref
-         linkend="guc-autovacuum-freeze-max-age"/>.
+         linkend="guc-autovacuum-freeze-max-age"/>.  For more
+        information see <xref linkend="vacuum-xid-exhaustion"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9366,9 +9364,11 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         Although users can set this value anywhere from zero to two billion,
         <command>VACUUM</command> will silently limit the effective value to 95% of
         <xref linkend="guc-autovacuum-multixact-freeze-max-age"/>, so that a
-        periodic manual <command>VACUUM</command> has a chance to run before an
-        anti-wraparound is launched for the table.
-        For more information see <xref linkend="vacuum-for-multixact-wraparound"/>.
+        standard autovacuum (or a manual <command>VACUUM</command>) has a
+        chance to run using <command>VACUUM</command>'s aggressive strategy
+        before an anti-wraparound autovacuum is launched for the table.  For
+        more information see <xref linkend="vacuum-aggressive"/> and <xref
+         linkend="vacuum-antiwraparound-autovacuums"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9389,7 +9389,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         the value of <xref linkend="guc-autovacuum-multixact-freeze-max-age"/>,
         so that there is not an unreasonably short time between forced
         autovacuums.
-        For more information see <xref linkend="vacuum-for-multixact-wraparound"/>.
+        For more information see <xref linkend="vacuum-freezing-xid-space"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9406,9 +9406,11 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         <structname>pg_class</structname>.<structfield>relminmxid</structfield>
         field can attain before <command>VACUUM</command> takes
         extraordinary measures to avoid system-wide multixact ID
-        wraparound failure.  This is <command>VACUUM</command>'s
-        strategy of last resort.  The failsafe typically triggers when
-        an autovacuum to prevent transaction ID wraparound has already
+        exhaustion (see <xref linkend="vacuum-xid-exhaustion"/>).
+        This is <command>VACUUM</command>'s strategy of last resort.  The
+        failsafe typically triggers when an
+        <link linkend="vacuum-antiwraparound-autovacuums">anti-wraparound
+         autovacuum</link> has already
         been running for some time, though it's possible for the
         failsafe to trigger during any <command>VACUUM</command>.
        </para>
@@ -9422,7 +9424,8 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         this value anywhere from zero to 2.1 billion,
         <command>VACUUM</command> will silently adjust the effective
         value to no less than 105% of <xref
-         linkend="guc-autovacuum-multixact-freeze-max-age"/>.
+         linkend="guc-autovacuum-multixact-freeze-max-age"/>.  For more
+        information see <xref linkend="vacuum-xid-exhaustion"/>.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index cbd3aa804..cc6499e36 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -352,8 +352,8 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
       even when there is no connection using them. This consumes storage
       because neither required WAL nor required rows from the system catalogs
       can be removed by <command>VACUUM</command> as long as they are required by a replication
-      slot.  In extreme cases this could cause the database to shut down to prevent
-      transaction ID wraparound (see <xref linkend="vacuum-for-wraparound"/>).
+      slot.  In extreme cases this could cause the database to refuse to allocate new
+      transaction IDs (see <xref linkend="vacuum-xid-exhaustion"/>).
       So if a slot is no longer required it should be dropped.
      </para>
     </caution>
diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml
index f00442564..abdf01009 100644
--- a/doc/src/sgml/maintenance.sgml
+++ b/doc/src/sgml/maintenance.sgml
@@ -148,13 +148,8 @@ vacuum insert threshold = vacuum base insert threshold + vacuum insert scale fac
      <xref linkend="guc-autovacuum-vacuum-insert-scale-factor"/>.
      Such vacuums may allow portions of the table to be marked as
      <firstterm>all visible</firstterm> and also allow tuples to be frozen, which
-     can reduce the work required in subsequent vacuums.
-     For tables which receive <command>INSERT</command> operations but no or
-     almost no <command>UPDATE</command>/<command>DELETE</command> operations,
-     it may be beneficial to lower the table's
-     <xref linkend="reloption-autovacuum-freeze-min-age"/> as this may allow
-     tuples to be frozen by earlier vacuums.  The number of obsolete tuples and
-     the number of inserted tuples are obtained from the cumulative statistics system;
+     can reduce the work required in subsequent vacuums.  The number of obsolete tuples
+     and the number of inserted tuples are obtained from the cumulative statistics system;
      it is a semi-accurate count updated by each <command>UPDATE</command>,
      <command>DELETE</command> and <command>INSERT</command> operation.  (It is
      only semi-accurate because some information might be lost under heavy
@@ -211,10 +206,11 @@ analyze threshold = analyze base threshold + analyze scale factor * number of tu
     <literal>SHARE UPDATE EXCLUSIVE</literal> lock held by autovacuum, lock
     acquisition will interrupt the autovacuum.  For conflicting lock modes,
     see <xref linkend="table-lock-compatibility"/>.  However, if the autovacuum
-    is running to prevent transaction ID wraparound (i.e., the autovacuum query
-    name in the <structname>pg_stat_activity</structname> view ends with
+    is an anti-wraparound autovacuum (i.e., the autovacuum query name in the
+    <structname>pg_stat_activity</structname> view ends with
     <literal>(to prevent wraparound)</literal>), the autovacuum is not
-    automatically interrupted.
+    automatically interrupted.  See <xref linkend="vacuum-antiwraparound-autovacuums"/>
+    for more details on anti-wraparound autovacuums.
    </para>
 
    <warning>
@@ -272,15 +268,21 @@ analyze threshold = analyze base threshold + analyze scale factor * number of tu
     </listitem>
 
     <listitem>
-     <simpara>To protect against loss of very old data due to
-     <firstterm>transaction ID wraparound</firstterm> or
-     <firstterm>multixact ID wraparound</firstterm>.</simpara>
+     <simpara>To maintain the system's ability to allocate transaction IDs
+      through freezing.</simpara>
     </listitem>
 
     <listitem>
      <simpara>To update the visibility map, which speeds
      up <link linkend="indexes-index-only-scans">index-only
-     scans</link>.</simpara>
+     scans</link>, and helps the next <command>VACUUM</command>
+     operation avoid needlessly scanning already-frozen pages.</simpara>
+    </listitem>
+
+    <listitem>
+     <simpara>To enable truncation of obsolescent transaction status
+      information in structures such as <filename>pg_xact</filename> for the
+      entire cluster.</simpara>
     </listitem>
 
     <listitem>
@@ -477,303 +479,756 @@ analyze threshold = analyze base threshold + analyze scale factor * number of tu
    </warning>
   </sect2>
 
-  <sect2 id="vacuum-for-wraparound">
-   <title>Preventing Transaction ID Wraparound Failures</title>
-
-   <indexterm zone="vacuum-for-wraparound">
-    <primary>transaction ID</primary>
-    <secondary>wraparound</secondary>
-   </indexterm>
+  <sect2 id="vacuum-freezing-xid-space">
+   <title>Freezing to manage the transaction ID space</title>
 
    <indexterm>
-    <primary>wraparound</primary>
-    <secondary>of transaction IDs</secondary>
+    <primary>Freezing</primary>
+    <secondary>of transaction IDs and Multixact IDs</secondary>
    </indexterm>
 
    <para>
-    <productname>PostgreSQL</productname>'s <link
-     linkend="mvcc-intro">MVCC</link> transaction semantics depend on
-    being able to compare <glossterm linkend="glossary-xid">transaction
-     ID numbers (<acronym>XID</acronym>)</glossterm> to determine
-    whether or not the row is visible to each query's MVCC snapshot
-    (see <xref linkend="interpreting-xid-stamps"/>).  But since
-    on-disk storage of transaction IDs in heap pages uses a truncated
-    32-bit representation to save space (rather than the full 64-bit
-    representation), it is necessary to vacuum every table in every
-    database <emphasis>at least</emphasis> once every two billion
-    transactions (though far more frequent vacuuming is typical).
+    <command>VACUUM</command> often marks some of the pages that it scans
+    <emphasis>frozen</emphasis>, indicating that all eligible rows on the page
+    were inserted by a transaction that committed sufficiently far in the past
+    that the effects of the inserting transaction are certain to be visible to
+    all current and future transactions.  The specific transaction ID number
+    (<acronym>XID</acronym>) stored in a frozen heap row's
+    <structfield>xmin</structfield> field is no longer needed to determine its
+    visibility.  Furthermore, when a row undergoing freezing has an XID set in
+    its <structfield>xmax</structfield> field (e.g., an XID left behind by an
+    earlier <command>SELECT FOR UPDATE</command> row locker), the
+    <structfield>xmax</structfield> field's XID is usually also removed.
    </para>
 
    <para>
-    <xref linkend="guc-vacuum-freeze-min-age"/>
-    controls how old an XID value has to be before rows bearing that XID will be
-    frozen.  Increasing this setting may avoid unnecessary work if the
-    rows that would otherwise be frozen will soon be modified again,
-    but decreasing this setting increases
-    the number of transactions that can elapse before the table must be
-    vacuumed again.
+    Once frozen, heap pages are <quote>self-contained</quote>.  Every query
+    can read all of the page's rows in a way that assumes that the inserting
+    transaction committed and is visible to its <acronym>MVCC</acronym>
+    snapshot.  No query will ever have to consult external transaction status
+    metadata to interpret the page's contents, either.  In particular,
+    <filename>pg_xact</filename> transaction XID commit/abort status lookups
+    won't occur during query execution.
    </para>
 
    <para>
-    <command>VACUUM</command> uses the <link linkend="storage-vm">visibility map</link>
-    to determine which pages of a table must be scanned.  Normally, it
-    will skip pages that don't have any dead row versions even if those pages
-    might still have row versions with old XID values.  Therefore, normal
-    <command>VACUUM</command>s won't always freeze every old row version in the table.
-    When that happens, <command>VACUUM</command> will eventually need to perform an
-    <firstterm>aggressive vacuum</firstterm>, which will freeze all eligible unfrozen
-    XID and MXID values, including those from all-visible but not all-frozen pages.
-    In practice most tables require periodic aggressive vacuuming.
-    <xref linkend="guc-vacuum-freeze-table-age"/>
-    controls when <command>VACUUM</command> does that: all-visible but not all-frozen
-    pages are scanned if the number of transactions that have passed since the
-    last such scan is greater than <varname>vacuum_freeze_table_age</varname> minus
-    <varname>vacuum_freeze_min_age</varname>. Setting
-    <varname>vacuum_freeze_table_age</varname> to 0 forces <command>VACUUM</command> to
-    always use its aggressive strategy.
+    Freezing is a <acronym>WAL</acronym>-logged operation, so when
+    <command>VACUUM</command> freezes a heap page, any copy of the page
+    located on a physical replication standby server will itself be
+    <quote>frozen</quote> shortly thereafter (when the relevant
+    <literal>FREEZE_PAGE</literal> <acronym>WAL</acronym> record is replayed
+    on the standby).  Queries that run on physical replication standbys avoid
+    <filename>pg_xact</filename> lookups when reading from frozen pages, just
+    like queries that run on the primary server
+    <footnote>
+     <para>
+      In this regard, freezing is unlike setting transaction status
+      <quote>hint bits</quote> in tuple headers: setting hint bits doesn't
+      usually need to be <acronym>WAL</acronym>-logged, and can take place on
+      physical replication standby servers without input from the primary
+      server.  Hint bits exist to allow query execution to avoid repeated
+      <filename>pg_xact</filename> lookups for the same tuples, strictly as an
+      optimization.  On the other hand, freezing exists because the system
+      needs to reliably remove <filename>pg_xact</filename> dependencies from
+      individual tuples.
+     </para>
+    </footnote>.
    </para>
 
    <para>
-    The maximum time that a table can go unvacuumed is two billion
-    transactions minus the <varname>vacuum_freeze_min_age</varname> value at
-    the time of the last aggressive vacuum. If it were to go
-    unvacuumed for longer than
-    that, data loss could result.  To ensure that this does not happen,
-    autovacuum is invoked on any table that might contain unfrozen rows with
-    XIDs older than the age specified by the configuration parameter <xref
-    linkend="guc-autovacuum-freeze-max-age"/>.  (This will happen even if
-    autovacuum is disabled.)
+    <command>VACUUM</command> generally postpones some freezing work as an
+    optimization, but <command>VACUUM</command> cannot delay freezing forever.
+    Since on-disk storage of transaction IDs in heap row headers uses a
+    truncated partial 32-bit representation to save space (rather than the
+    full 64-bit representation used in other contexts), it plays a crucial
+    role in enabling <link linkend="vacuum-aggressive">management of the XID
+     address space</link> by <command>VACUUM</command>.  If, for whatever
+    reason, <command>VACUUM</command> is unable to freeze older XIDs on behalf
+    of an application that continues to require XID allocations, the system
+    will eventually <link linkend="vacuum-xid-exhaustion">refuse to allocate
+     transaction IDs</link> due to transaction ID exhaustion (though this is
+    unlikely to occur unless autovacuum is configured incorrectly).
    </para>
 
    <para>
-    This implies that if a table is not otherwise vacuumed,
-    autovacuum will be invoked on it approximately once every
-    <varname>autovacuum_freeze_max_age</varname> minus
-    <varname>vacuum_freeze_min_age</varname> transactions.
-    For tables that are regularly vacuumed for space reclamation purposes,
-    this is of little importance.  However, for static tables
-    (including tables that receive inserts, but no updates or deletes),
-    there is no need to vacuum for space reclamation, so it can
-    be useful to try to maximize the interval between forced autovacuums
-    on very large static tables.  Obviously one can do this either by
-    increasing <varname>autovacuum_freeze_max_age</varname> or decreasing
-    <varname>vacuum_freeze_min_age</varname>.
+    <xref linkend="guc-vacuum-freeze-min-age"/> controls when freezing takes
+    place.  When <command>VACUUM</command> scans a heap page containing even
+    one XID that has already attained an age exceeding this value, the page is
+    frozen.
+   </para>
+
+   <indexterm>
+    <primary>Multixact ID</primary>
+    <secondary>Freezing of</secondary>
+   </indexterm>
+
+   <para>
+    <firstterm>Multixact IDs</firstterm> support row locking by multiple
+    transactions.  Since there is only limited space in a <link
+     linkend="storage-tuple-layout">heap tuple header</link> to store lock
+    information, that information is encoded as a <quote>multiple transaction
+     ID</quote>, or Multixact ID for short, whenever there is more than one
+    transaction concurrently locking a row.  Information about which
+    transaction IDs are included in any particular Multixact ID is stored
+    separately in <filename>pg_multixact</filename>.  Only the Multixact ID
+    itself (a 32-bit integer) appears in the tuple's
+    <structfield>xmax</structfield> field.  This creates a dependency on
+    external Multixact ID transaction status information.  This is similar to
+    the dependency ordinary unfrozen XIDs have on commit status information
+    from <filename>pg_xact</filename>.  <command>VACUUM</command> must
+    therefore occasionally remove Multixact IDs from tuples during freezing.
    </para>
 
    <para>
-    The effective maximum for <varname>vacuum_freeze_table_age</varname> is 0.95 *
-    <varname>autovacuum_freeze_max_age</varname>; a setting higher than that will be
-    capped to the maximum. A value higher than
-    <varname>autovacuum_freeze_max_age</varname> wouldn't make sense because an
-    anti-wraparound autovacuum would be triggered at that point anyway, and
-    the 0.95 multiplier leaves some breathing room to run a manual
-    <command>VACUUM</command> before that happens.  As a rule of thumb,
-    <command>vacuum_freeze_table_age</command> should be set to a value somewhat
-    below <varname>autovacuum_freeze_max_age</varname>, leaving enough gap so that
-    a regularly scheduled <command>VACUUM</command> or an autovacuum triggered by
-    normal delete and update activity is run in that window.  Setting it too
-    close could lead to anti-wraparound autovacuums, even though the table
-    was recently vacuumed to reclaim space, whereas lower values lead to more
-    frequent aggressive vacuuming.
+    <xref linkend="guc-vacuum-multixact-freeze-min-age"/> also controls when
+    freezing takes place.  It is analogous to
+    <varname>vacuum_freeze_min_age</varname>, but <quote>age</quote> is
+    expressed in Multixact ID units.  Lowering
+    <varname>vacuum_multixact_freeze_min_age</varname>
+    <emphasis>forces</emphasis> <command>VACUUM</command> to process
+    <structfield>xmax</structfield> fields with a Multixact ID in cases where
+    it would otherwise postpone the work of processing
+    <structfield>xmax</structfield> until the next <command>VACUUM</command>
+    <footnote>
+     <para>
+      <quote>Freezing</quote> of <structfield>xmax</structfield> fields
+      (whether they contain an XID or a Multixact ID) generally means clearing
+      <structfield>xmax</structfield> from a tuple header.
+      <command>VACUUM</command> may occasionally encounter an individual
+      Multixact ID that must be removed to advance the table's
+      <structfield>relminmxid</structfield> by the required amount, which can
+      only be processed by generating a replacement Multixact ID (containing
+      just the non-removable subset of member XIDs from the original Multixact
+      ID), and then setting <structfield>xmax</structfield> to the
+      new/replacement Multixact ID value.
+     </para>
+    </footnote>.  The setting generally doesn't significantly influence the
+    total number of pages <command>VACUUM</command> freezes, even in tables
+    containing many Multixact IDs.  This is because <command>VACUUM</command>
+    generally prefers proactive processing for most individual
+    <structfield>xmax</structfield> fields that contain a Multixact ID (eager
+    proactive processing is typically cheaper).
    </para>
 
    <para>
-    The sole disadvantage of increasing <varname>autovacuum_freeze_max_age</varname>
-    (and <varname>vacuum_freeze_table_age</varname> along with it) is that
-    the <filename>pg_xact</filename> and <filename>pg_commit_ts</filename>
-    subdirectories of the database cluster will take more space, because it
-    must store the commit status and (if <varname>track_commit_timestamp</varname> is
-    enabled) timestamp of all transactions back to
-    the <varname>autovacuum_freeze_max_age</varname> horizon.  The commit status uses
-    two bits per transaction, so if
-    <varname>autovacuum_freeze_max_age</varname> is set to its maximum allowed value
-    of two billion, <filename>pg_xact</filename> can be expected to grow to about half
-    a gigabyte and <filename>pg_commit_ts</filename> to about 20GB.  If this
-    is trivial compared to your total database size,
-    setting <varname>autovacuum_freeze_max_age</varname> to its maximum allowed value
-    is recommended.  Otherwise, set it depending on what you are willing to
-    allow for <filename>pg_xact</filename> and <filename>pg_commit_ts</filename> storage.
-    (The default, 200 million transactions, translates to about 50MB
-    of <filename>pg_xact</filename> storage and about 2GB of <filename>pg_commit_ts</filename>
-    storage.)
+    Managing the added <acronym>WAL</acronym> volume from freezing over time
+    is a vital consideration for <command>VACUUM</command>.  It is why
+    <command>VACUUM</command> doesn't just freeze every eligible tuple at the
+    earliest opportunity: the <acronym>WAL</acronym> written to freeze a
+    page's tuples is wasted in cases where the resulting frozen tuples are
+    soon deleted or updated anyway.  It's also why <command>VACUUM</command>
+    <emphasis>will</emphasis> freeze all eligible tuples from a heap page once
+    the decision to freeze at least one tuple is taken: at that point, the
+    added cost of freezing all eligible tuples eagerly (measured in
+    <quote>extra bytes of <acronym>WAL</acronym> written</quote>) is far lower
+    than the probable cost of deferring freezing until a future
+    <command>VACUUM</command> operation against the same table.  Furthermore,
+    once the page is frozen, it can generally be <link
+     linkend="vacuum-for-visibility-map">marked as all-frozen within the
+     visibility map</link> immediately afterwards.
    </para>
 
-   <para>
-    One disadvantage of decreasing <varname>vacuum_freeze_min_age</varname> is that
-    it might cause <command>VACUUM</command> to do useless work: freezing a row
-    version is a waste of time if the row is modified
-    soon thereafter (causing it to acquire a new XID).  So the setting should
-    be large enough that rows are not frozen until they are unlikely to change
-    any more.
-   </para>
+   <note>
+    <para>
+     In <productname>PostgreSQL</productname> versions before 16,
+     <command>VACUUM</command> triggered freezing at the level of individual
+     <structfield>xmin</structfield> and <structfield>xmax</structfield>
+     fields.  Freezing only affected the exact XIDs that had already attained
+     an age of <varname>vacuum_freeze_min_age</varname> or greater.
+    </para>
+   </note>
 
    <para>
-    To track the age of the oldest unfrozen XIDs in a database,
-    <command>VACUUM</command> stores XID
-    statistics in the system tables <structname>pg_class</structname> and
-    <structname>pg_database</structname>.  In particular,
-    the <structfield>relfrozenxid</structfield> column of a table's
-    <structname>pg_class</structname> row contains the oldest remaining unfrozen
-    XID at the end of the most recent <command>VACUUM</command> that successfully
-    advanced <structfield>relfrozenxid</structfield> (typically the most recent
-    aggressive VACUUM).  Similarly, the
-    <structfield>datfrozenxid</structfield> column of a database's
-    <structname>pg_database</structname> row is a lower bound on the unfrozen XIDs
-    appearing in that database &mdash; it is just the minimum of the
-    per-table <structfield>relfrozenxid</structfield> values within the database.
-    A convenient way to
-    examine this information is to execute queries such as:
-
-<programlisting>
-SELECT c.oid::regclass as table_name,
-       greatest(age(c.relfrozenxid),age(t.relfrozenxid)) as age
-FROM pg_class c
-LEFT JOIN pg_class t ON c.reltoastrelid = t.oid
-WHERE c.relkind IN ('r', 'm');
-
-SELECT datname, age(datfrozenxid) FROM pg_database;
-</programlisting>
-
-    The <literal>age</literal> column measures the number of transactions from the
-    cutoff XID to the current transaction's XID.
+    <command>VACUUM</command> also triggers the freezing of a page in cases
+    where it already proved necessary to write out a full page image
+    (<acronym>FPI</acronym>) as part of a <acronym>WAL</acronym> record
+    describing how dead tuples were removed <footnote>
+     <para>
+      Actually, the <quote>freeze on an <acronym>FPI</acronym> write</quote>
+      mechanism isn't just used when <command>VACUUM</command> needs to
+      generate an <acronym>FPI</acronym> (as torn page protection) for
+      inclusion in a <acronym>WAL</acronym> record describing how dead tuples
+      were removed.  The <acronym>FPI</acronym> mechanism also triggers when
+      hint bits are set by <command>VACUUM</command>, if and only if it
+      necessitates writing an <acronym>FPI</acronym>.  The need to write a
+      <acronym>WAL</acronym> record to set hint bits only arises when
+      <xref linkend="guc-wal-log-hints"/> is enabled in
+      <filename>postgresql.conf</filename>, or when data checksums were
+      enabled when the cluster was initialized with <xref linkend="app-initdb"/>.
+     </para>
+    </footnote> (see <xref linkend="wal-reliability"/> for background
+    information about how <acronym>FPI</acronym>s provide torn page
+    protection).  This <quote>freeze on an <acronym>FPI</acronym>
+     write</quote> batching mechanism avoids an expected additional
+    <acronym>FPI</acronym> for the same page later on (this is the probable
+    outcome of lazily deferring freezing until <varname>vacuum_freeze_min_age</varname>
+    forces it).  In effect, <command>VACUUM</command> generates slightly more
+    <acronym>WAL</acronym> in the short term with the aim of ultimately
+    needing to generate much less <acronym>WAL</acronym> in the long term.
    </para>
 
    <tip>
     <para>
-     When the <command>VACUUM</command> command's <literal>VERBOSE</literal>
-     parameter is specified, <command>VACUUM</command> prints various
-     statistics about the table.  This includes information about how
-     <structfield>relfrozenxid</structfield> and
-     <structfield>relminmxid</structfield> advanced, and the number of
-     newly frozen pages.  The same details appear in the server log when
-     autovacuum logging (controlled by <xref
-      linkend="guc-log-autovacuum-min-duration"/>) reports on a
-     <command>VACUUM</command> operation executed by autovacuum.
+     For tables that receive <command>INSERT</command> operations, but few or
+     no <command>UPDATE</command>/<command>DELETE</command> operations, it
+     might be beneficial to lower <xref linkend="reloption-autovacuum-freeze-min-age"/>
+     for the table.  This makes <command>VACUUM</command> freeze the table's
+     pages <quote>eagerly</quote> during earlier autovacuums triggered by
+     <xref linkend="guc-autovacuum-vacuum-insert-scale-factor"/>, which
+     improves performance stability for some workloads.
     </para>
    </tip>
 
-   <para>
-    <command>VACUUM</command> normally only scans pages that have been modified
-    since the last vacuum, but <structfield>relfrozenxid</structfield> can only be
-    advanced when every page of the table
-    that might contain unfrozen XIDs is scanned.  This happens when
-    <structfield>relfrozenxid</structfield> is more than
-    <varname>vacuum_freeze_table_age</varname> transactions old, when
-    <command>VACUUM</command>'s <literal>FREEZE</literal> option is used, or when all
-    pages that are not already all-frozen happen to
-    require vacuuming to remove dead row versions. When <command>VACUUM</command>
-    scans every page in the table that is not already all-frozen, it should
-    set <literal>age(relfrozenxid)</literal> to a value just a little more than the
-    <varname>vacuum_freeze_min_age</varname> setting
-    that was used (more by the number of transactions started since the
-    <command>VACUUM</command> started).  <command>VACUUM</command>
-    will set <structfield>relfrozenxid</structfield> to the oldest XID
-    that remains in the table, so it's possible that the final value
-    will be much more recent than strictly required.
-    If no <structfield>relfrozenxid</structfield>-advancing
-    <command>VACUUM</command> is issued on the table until
-    <varname>autovacuum_freeze_max_age</varname> is reached, an autovacuum will soon
-    be forced for the table.
-   </para>
+   <sect3 id="vacuum-aggressive">
+    <title>Aggressive <command>VACUUM</command></title>
 
-   <para>
-    If for some reason autovacuum fails to clear old XIDs from a table, the
-    system will begin to emit warning messages like this when the database's
-    oldest XIDs reach forty million transactions from the wraparound point:
+    <indexterm zone="vacuum-aggressive">
+     <primary>transaction ID</primary>
+     <secondary>wraparound</secondary>
+    </indexterm>
+
+    <indexterm>
+     <primary>wraparound</primary>
+     <secondary>of transaction IDs and Multixact IDs</secondary>
+    </indexterm>
+
+    <para>
+     As noted already, freezing doesn't just allow queries to avoid lookups of
+     subsidiary transaction status information in structures such as
+     <filename>pg_xact</filename>.  Freezing also plays a crucial role in
+     enabling transaction ID address space management by
+     <command>VACUUM</command> (and autovacuum).  <command>VACUUM</command>
+     maintains information about the oldest unfrozen XID that remains in the
+     table when it uses its <firstterm>aggressive strategy</firstterm>.
+    </para>
+
+    <para>
+     Aggressive <command>VACUUM</command> updates the table's <link
+      linkend="catalog-pg-class"><structname>pg_class</structname></link>.<structfield>relfrozenxid</structfield>
+     to whatever XID was the oldest observed XID that
+     <command>VACUUM</command> <emphasis>didn't</emphasis> freeze still
+     remaining at the end of processing.  The table's
+     <structfield>relfrozenxid</structfield> <quote>advances</quote> by a
+     certain number of XIDs (relative to the previous value set during the last
+     aggressive <command>VACUUM</command>) as progress on freezing the oldest
+     pages in the table permits.  Aggressive <command>VACUUM</command> will
+     occasionally need to advance the whole database's <link
+      linkend="catalog-pg-database"><structname>pg_database</structname></link>.<structfield>datfrozenxid</structfield>
+     afterwards, too &mdash; this is the minimum of the per-table
+     <structfield>relfrozenxid</structfield> values (i.e., the earliest
+     <structfield>relfrozenxid</structfield>) within the database.
+    </para>
+
+    <para>
+     Aggressive <command>VACUUM</command> may need to perform significant
+     amounts of <quote>catch-up</quote> freezing missed by earlier
+     non-aggressive <command>VACUUM</command>s, because non-aggressive
+     <command>VACUUM</command> sometimes allows unfrozen pages to build up.
+    </para>
+
+    <para>
+     Over time, aggressive autovacuuming has two beneficial effects on the
+     system as a whole:
+
+     <orderedlist>
+      <listitem>
+       <simpara>It keeps track of the oldest remaining unfrozen transaction ID
+        in the entire <glossterm linkend="glossary-db-cluster">database
+         cluster</glossterm> (i.e., the oldest transaction ID across every
+        table in every database).</simpara>
+      </listitem>
+      <listitem>
+       <simpara>It avoids a cluster-wide oldest unfrozen transaction ID that
+        is <quote>too old</quote>.</simpara>
+      </listitem>
+     </orderedlist>
+    </para>
+
+    <para>
+     The maximum XID age that the system can tolerate (i.e., the maximum
+     <quote>distance</quote> between the oldest unfrozen transaction ID in any
+     table in the database cluster and the next unallocated transaction ID) is
+     about 2.1 billion transaction IDs.  This <quote>maximum XID age</quote>
+     invariant makes it fundamentally impossible to postpone aggressive
+     <command>VACUUM</command>s (and freezing) forever.  While there is no
+     simple formula for determining an oldest XID <quote>age</quote> for
+     database administrators to target, the invariant imposes a 2.1 billion
+     XID age hard limit &mdash; so there <emphasis>is</emphasis> a clear point
+     at which unfrozen XIDs should <emphasis>always</emphasis> be considered
+     <quote>too old</quote>, regardless of individual application requirements
+     or workload characteristics.  If the hard limit is reached, the system
+     experiences <link linkend="vacuum-xid-exhaustion">transaction ID
+      exhaustion</link>, which temporarily prevents the allocation of new
+     permanent transaction IDs.  The system will only regain the ability to
+     allocate new transaction IDs when <command>VACUUM</command> succeeds in
+     advancing the oldest <literal>datfrozenxid</literal> in the cluster
+     (following an aggressive <command>VACUUM</command> that runs to
+     completion against the table with the oldest
+     <structfield>relfrozenxid</structfield>).
+    </para>
+
+    <para>
+     Aggressive <command>VACUUM</command> also maintains the <link
+      linkend="catalog-pg-class"><structname>pg_class</structname></link>.<structfield>relminmxid</structfield> and
+     <link linkend="catalog-pg-database"><structname>pg_database</structname></link>.<structfield>datminmxid</structfield>
+     fields.  These are needed to track the oldest Multixact ID in the table
+     and database, respectively.  There are analogous rules, driven by
+     analogous considerations about managing the Multixact ID space.  This
+     doesn't usually affect aggressive vacuuming requirements to a noticeable
+     degree, but can in databases that consume more Multixact IDs than
+     transaction IDs.
+    </para>
+
+    <caution>
+     <para>
+      <command>VACUUM</command> may not always freeze tuple
+      <structfield>xmin</structfield> XIDs that have reached
+      <varname>vacuum_freeze_min_age</varname> in age.  The basic eligibility
+      criteria for freezing is the same as the criteria that determines if a
+      deleted tuple is safe for <command>VACUUM</command> to remove: the
+      XID-based <literal>removable cutoff</literal> (this is one of the
+      details that appears in the server log's reports on autovacuum
+      <footnote id="vacuum-autovacuum-log">
+       <para>
+        Autovacuum's log reports appear in the server log for autovacuums
+        whose <command>VACUUM</command> takes longer than a threshold
+        controlled by <xref linkend="guc-log-autovacuum-min-duration"/>.
+        Manual <command>VACUUM</command>s output the same details as
+        <literal>INFO</literal> messages when the <command>VACUUM</command>
+        command's <literal>VERBOSE</literal> option is used (note that manual
+        <command>VACUUM</command>s never generate reports in the server log).
+       </para>
+      </footnote>).
+     </para>
+     <para>
+      In extreme cases, a long-running transaction can hold back every
+      <command>VACUUM</command>'s <literal>removable cutoff</literal> for so
+      long that the system experiences
+      <link linkend="vacuum-xid-exhaustion">transaction ID exhaustion</link>.
+      See <xref linkend="monitoring-table-age"/> for details on how to monitor
+      <structfield>relfrozenxid</structfield> and
+      <structfield>relminmxid</structfield> age to avoid
+      transaction ID/Multixact ID exhaustion.
+     </para>
+     <para>
+      These issues can be debugged by following autovacuum log reports from
+      the server log over time: the log reports will include information about
+      the age of each <command>VACUUM</command>'s <literal>removable
+       cutoff</literal> at the point the <command>VACUUM</command> ended.
+      It may be useful to correlate the use of a cutoff with an excessively
+      high age with application-level problems such as long-running
+      transactions.
+     </para>
+    </caution>
+
+    <para>
+     The 2.1 billion XIDs <quote>maximum XID age</quote> invariant must be
+     preserved because transaction IDs stored in <link
+      linkend="storage-tuple-layout">heap tuple headers</link> use a truncated
+     32-bit representation (rather than the full 64-bit representation used in
+     other contexts).  Since all unfrozen transaction IDs from heap tuple
+     headers <emphasis>must</emphasis> be from the same transaction ID epoch
+     (or from a space in the 64-bit representation that spans two adjoining
+     transaction ID epochs), there isn't any need to include a separate epoch
+     field in each tuple header (see <xref linkend="interpreting-xid-stamps"/>
+     for further details).  This scheme requires much less on-disk storage
+     space than a design that stores full 64-bit XIDs (consisting of a 32-bit
+     epoch and a 32-bit partial XID) in heap tuple headers.  On the other
+     hand, it constrains the system's ability to allocate new XIDs in the
+     worst case scenario where transaction ID exhaustion occurs.
+    </para>
+
+    <para>
+     There is only one <emphasis>major</emphasis> behavioral difference
+     between aggressive <command>VACUUM</command> and non-aggressive
+     <command>VACUUM</command>: non-aggressive <command>VACUUM</command> skips
+     pages marked as all-visible using the visibility map, whereas aggressive
+     <command>VACUUM</command> only skips the subset of pages that are both
+     all-visible <emphasis>and</emphasis> all-frozen.  In other words, pages
+     that are <emphasis>just</emphasis> all-visible at the beginning of an
+     aggressive <command>VACUUM</command> must be scanned, not skipped.
+     Scanning existing all-visible pages is necessary to determine the oldest
+     unfrozen XID that will remain in the table at the end of an aggressive
+     <command>VACUUM</command>.
+    </para>
+
+    <note>
+     <para>
+      In practice, most tables require periodic aggressive vacuuming.
+      However, some individual non-aggressive <command>VACUUM</command>
+      operations can advance the table's
+      <structfield>relfrozenxid</structfield> and/or
+      <structfield>relminmxid</structfield>.
+     </para>
+     <para>
+      This happens whenever a non-aggressive <command>VACUUM</command> notices
+      that it is safe without incurring any added cost from scanning
+      <quote>extra</quote> pages.  It is most common in small, frequently
+      modified tables.
+     </para>
+    </note>
+
+    <para>
+     Non-aggressive <command>VACUUM</command>s can sometimes overlook older
+     XIDs from existing all-visible pages (due to their policy of always
+     skipping all-visible pages).  Over time, this can even lead to a
+     significant build-up of unfrozen pages in one table (accumulated
+     all-visible pages that remain unfrozen).  When that happens, it is
+     inevitable that an aggressive <command>VACUUM</command> will eventually
+     need to perform <quote>catch-up</quote> freezing that clears the table's
+     backlog of unfrozen pages.
+    </para>
+
+    <para>
+     There is also one <emphasis>minor</emphasis> behavioral difference
+     between aggressive <command>VACUUM</command> and non-aggressive
+     <command>VACUUM</command>:  only aggressive <command>VACUUM</command> is
+     required to sometimes wait for a page-level cleanup lock when a page is
+     scanned and observed to contain transaction IDs/Multixact IDs that
+     <emphasis>must</emphasis> be frozen.  This difference exists because
+     aggressive <command>VACUUM</command> is strictly required to advance
+     <structfield>relfrozenxid</structfield> and/or
+     <structfield>relminmxid</structfield> to
+     <emphasis>sufficiently</emphasis> recent values
+     <footnote>
+      <para>
+       Aggressive <command>VACUUM</command> is (somewhat arbitrarily) required
+       to freeze all pages containing transaction IDs older than
+       <varname>vacuum_freeze_min_age</varname> and/or Multixact ID values
+       older than <varname>vacuum_multixact_freeze_min_age</varname>, at a
+       minimum.
+      </para>
+     </footnote>.  The behavior can lead to occasional waits for a conflicting
+     buffer pin to be released by another backend.  These waits are
+     imperceptible and harmless most of the time.  In extreme cases there can
+     be extended waits, which can be observed under the
+     <literal>BufferPin</literal> wait event in the
+     <structname>pg_stat_activity</structname> view.  See <xref
+      linkend="wait-event-table"/>.
+    </para>
+
+    <note>
+     <para>
+      <quote>Catch-up</quote> freezing is not caused by any difference in how
+      <varname>vacuum_freeze_min_age</varname> is applied by each type of
+      <command>VACUUM</command>.  It is an indirect result of
+      <varname>vacuum_freeze_min_age</varname> only being applied to those
+      pages that <command>VACUUM</command> scans (and cleanup locks) in the
+      first place.  Therefore, it can be difficult to tune
+      <varname>vacuum_freeze_min_age</varname>, especially for tables that
+      receive frequent non-aggressive <command>VACUUM</command>s and
+      infrequent aggressive <command>VACUUM</command>s.
+     </para>
+    </note>
+
+    <tip>
+     <para>
+      Autovacuum server log reports <footnoteref
+       linkend="vacuum-autovacuum-log"/> show how many transaction IDs
+      <structfield>relfrozenxid</structfield> advanced by (if at all), and how
+      many Multixact IDs <structfield>relminmxid</structfield> advanced by (if
+      at all).
+     </para>
+     <para>
+      The number of pages frozen, and the number of pages scanned (i.e., the
+      number of pages processed because they could <emphasis>not</emphasis>
+      skipped using the visibility map) are also shown.  This can provide
+      useful guidance when tuning freezing-related settings, particularly
+      <varname>vacuum_freeze_table_age</varname> and
+      <varname>vacuum_freeze_min_age</varname>.
+     </para>
+    </tip>
+
+    <para>
+     <xref linkend="guc-vacuum-freeze-table-age"/> controls when
+     <command>VACUUM</command> uses its aggressive strategy.  If
+     <literal>age(relfrozenxid)</literal> exceeds
+     <varname>vacuum_freeze_table_age</varname> at the start of
+     <command>VACUUM</command>, <command>VACUUM</command> will employ its
+     aggressive strategy; otherwise, its standard non-aggressive strategy is
+     employed.  Setting <varname>vacuum_freeze_table_age</varname> to 0 forces
+     <command>VACUUM</command> to always use its aggressive strategy.
+    </para>
+
+    <para>
+     <xref linkend="guc-vacuum-multixact-freeze-table-age"/> also controls
+     when <command>VACUUM</command> uses its aggressive strategy.  This is an
+     independent Multixact ID based trigger for aggressive
+     <command>VACUUM</command>, which works just like
+     <varname>vacuum_freeze_table_age</varname>.  It is applied against
+     <literal>mxid_age(relminmxid)</literal> at the start of each
+     <command>VACUUM</command>.
+    </para>
+
+    <para>
+     It doesn't matter if it was <varname>vacuum_freeze_table_age</varname> or
+     <varname>vacuum_multixact_freeze_table_age</varname> that triggered
+     <command>VACUUM</command>'s decision to use its aggressive strategy.
+     <emphasis>Every</emphasis> aggressive <command>VACUUM</command> will
+     advance <structfield>relfrozenxid</structfield> and
+     <structfield>relminmxid</structfield> by applying the same generic policy
+     that controls which pages are frozen.
+    </para>
+
+    <para>
+     The default <varname>vacuum_freeze_table_age</varname> and
+     <varname>vacuum_multixact_freeze_table_age</varname> settings are
+     relatively low values.  The <varname>vacuum_freeze_table_age</varname>
+     and <varname>vacuum_freeze_min_age</varname> defaults are intended to
+     limit the system to using only about 10% of the available transaction ID
+     space at any one time.  This leaves the system with a generous amount of
+     <quote>slack capacity</quote> that allows XID allocations to continue in
+     the event of unforeseen problems with autovacuum and/or the application.
+     There might only be a negligible benefit from higher settings that aim to
+     reduce the number of <command>VACUUM</command>s that use the aggressive
+     strategy, in any case.  Some applications may even
+     <emphasis>benefit</emphasis> from tuning that makes autovacuum perform
+     aggressive <command>VACUUM</command>s more often.  If individual
+     aggressive <command>VACUUM</command>s can perform significantly less
+     <quote>catch-up</quote> freezing as a result, overall transaction
+     processing throughput is likely to be more stable and predictable.
+    </para>
+   </sect3>
+
+   <sect3 id="vacuum-antiwraparound-autovacuums">
+    <title>Anti-Wraparound Autovacuums</title>
+
+    <para>
+     To ensure that every table has its
+     <structfield>relfrozenxid</structfield> (and
+     <structfield>relminmxid</structfield>) advanced at regular intervals,
+     even in the case of completely static tables, autovacuum runs against any
+     table when it attains an age considered too far in the past.  These are
+     <firstterm>anti-wraparound autovacuums</firstterm>.  In practice, all
+     anti-wraparound autovacuums will use <command>VACUUM</command>'s
+     aggressive strategy (if they didn't, it would defeat the whole purpose of
+     anti-wraparound autovacuuming).
+    </para>
+
+    <para>
+     <xref linkend="guc-autovacuum-freeze-max-age"/> controls when the
+     autovacuum daemon launches anti-wraparound autovacuums.  If the
+     <literal>age(relfrozenxid)</literal> of a table exceeds
+     <varname>autovacuum_freeze_max_age</varname> when the autovacuum daemon
+     periodically examines the database (which happens once every <xref
+      linkend="guc-autovacuum-naptime"/> seconds), then an anti-wraparound
+     autovacuum is launched against the table.
+    </para>
+
+    <para>
+     <xref linkend="guc-autovacuum-multixact-freeze-max-age"/> also controls
+     when the autovacuum daemon launches anti-wraparound autovacuums.  It is
+     an independent Multixact ID based trigger for anti-wraparound
+     autovacuuming.  If the <literal>mxid_age(relminmxid)</literal> of a table
+     exceeds <varname>autovacuum_multixact_freeze_max_age</varname> when the
+     autovacuum daemon periodically examines the database
+     <footnote>
+      <para>
+       Autovacuum may use a lower <quote>effective</quote> Multixact ID age
+       than the <varname>autovacuum_multixact_freeze_max_age</varname> setting
+       in <filename>postgresql.conf</filename>, though.  Applying a lower
+       <quote>effective</quote> value like this avoids allowing the
+       <filename>pg_multixact/members</filename> <acronym>SLRU</acronym>
+       storage area to continue to grow in size for long, once its size
+       reaches <literal>2GB</literal>.
+       See <xref linkend="vacuum-truncate-xact-status"/>.
+      </para>
+     </footnote>, then an anti-wraparound autovacuum is launched against the
+     table.
+    </para>
+
+    <para>
+     Use of <command>VACUUM</command>'s aggressive strategy during
+     anti-wraparound autovacuuming is certain, because the effective value of
+     <varname>vacuum_freeze_table_age</varname> is silently limited to an
+     effective value no greater than 95% of the current value of
+     <varname>autovacuum_freeze_max_age</varname>.  Similarly, the effective
+     value of <varname>vacuum_multixact_freeze_table_age</varname> is silently
+     limited to a value no greater than 95% of the current value of
+     <varname>autovacuum_multixact_freeze_max_age</varname>.
+    </para>
+
+    <para>
+     It doesn't matter if it was <varname>autovacuum_freeze_max_age</varname>
+     or <varname>autovacuum_multixact_freeze_max_age</varname> that triggered
+     an anti-wraparound autovacuum.  <emphasis>Every</emphasis>
+     anti-wraparound autovacuum will be an aggressive
+     <command>VACUUM</command>, and will therefore advance
+     <structfield>relfrozenxid</structfield> and
+     <structfield>relminmxid</structfield> by applying the same generic policy
+     that controls which pages are frozen.
+    </para>
+
+    <para>
+     Anti-wraparound autovacuums are intended for static (and mostly static)
+     tables.  There is no reason to expect that a table receiving continual
+     row inserts and/or row modifications will ever require an anti-wraparound
+     autovacuum.  As a rule of thumb,
+     <varname>autovacuum_freeze_max_age</varname> should be set to a value
+     somewhat above <varname>vacuum_freeze_table_age</varname>, so that there
+     is a long window during which any autovacuum triggered by inserts,
+     updates, or deletes (or any manually issued <command>VACUUM</command>)
+     will become an aggressive <command>VACUUM</command>.  This has the
+     advantage of allowing aggressive vacuuming to take place at a time when
+     vacuuming was required anyway.  Each aggressive <command>VACUUM</command>
+     can therefore be expected to perform just as much useful work on
+     recovering disk space as an equivalent non-aggressive
+     <command>VACUUM</command> would have (had the non-aggressive strategy
+     been chosen instead).
+    </para>
+
+    <note>
+     <title>Aggressive/anti-wraparound differences</title>
+     <para>
+      Aggressive <command>VACUUM</command> is a special type of
+      <command>VACUUM</command>.  It must advance
+      <structfield>relfrozenxid</structfield> up to a value that was no
+      greater than <varname>vacuum_freeze_min_age</varname> in age as of the
+      <emphasis>start</emphasis> of the <command>VACUUM</command> operation.
+     </para>
+     <para>
+      Anti-wraparound autovacuum is a special type of autovacuum.  Its purpose
+      is to ensure that <structfield>relfrozenxid</structfield> advances when
+      no earlier <command>VACUUM</command> could advance it in passing &mdash;
+      often because no <command>VACUUM</command> has run against the table for
+      an extended period.
+     </para>
+     <para>
+      There is only one runtime behavioral difference between anti-wraparound
+      autovacuums and other autovacuums that run aggressive
+      <command>VACUUM</command>s: anti-wraparound autovacuums <emphasis>cannot
+       be autocancelled</emphasis>.  This means that autovacuum workers that
+      perform anti-wraparound autovacuuming do not yield to conflicting
+      relation-level lock requests (e.g., from <command>ALTER
+       TABLE</command>).  See <xref linkend="autovacuum-lock-conflicts"/> for
+      a full explanation.
+     </para>
+    </note>
+
+    <para>
+     In practice, anti-wraparound autovacuum is very likely to be the type of
+     autovacuum that updates the oldest <structfield>relfrozenxid</structfield>
+     in each database to a more recent value due to the presence of completely
+     static tables <footnote>
+      <para>
+       Anti-wraparound autovacuum is all but guaranteed to advance the oldest
+       <structfield>relfrozenxid</structfield>/<structfield>relminmxid</structfield>
+       (and therefore to advance
+       <structfield>datfrozenxid</structfield>/<structfield>datminmxid</structfield>)
+       in practice because in practice there is all but guaranteed to be at
+       least one totally static table that never gets an aggressive
+       <command>VACUUM</command> for any other reason (often just a tiny,
+       completely static system catalog table).
+      </para>
+     </footnote>.  As discussed in <xref linkend="vacuum-aggressive"/>,
+     <structfield>datfrozenxid</structfield> only advances when the oldest
+     <structfield>relfrozenxid</structfield> in the database advances
+     (<structfield>relminmxid</structfield> likewise only advances when the
+     earliest <structfield>datminmxid</structfield> in the database advances).
+     This implies that anti-wraparound autovacuum is <emphasis>also</emphasis>
+     very likely to be involved when any database's
+     <structfield>datfrozenxid</structfield>/<structfield>datminmxid</structfield>
+     advances (and when the cluster-wide earliest unfrozen transaction
+     ID/Multixact ID is advanced to a more recent value, in turn).
+    </para>
+
+    <para>
+     It follows that <varname>autovacuum_freeze_max_age</varname> is usually
+     the limiting factor for advancing the cluster-wide oldest unfrozen
+     transaction ID found in <link linkend="functions-info-controldata"><filename>pg_control</filename></link>
+     (the cluster-wide oldest unfrozen Multixact ID might occasionally be
+     influenced by <varname>autovacuum_multixact_freeze_max_age</varname>,
+     too).  This usually isn't much of a concern in itself, since it generally
+     doesn't predict anything about how far behind autovacuum is with freezing
+     physical heap pages.  Note, however, that this effect
+     <emphasis>can</emphasis> significantly impact the amount of space
+     required to store transaction status information.  The oldest transaction
+     status information (which is stored in external structures such as
+     <filename>pg_xact</filename>) cannot safely be truncated until
+     <command>VACUUM</command> can ascertain that there are no references to
+     the oldest entries remaining in any table, from any database.  See <xref
+      linkend="vacuum-truncate-xact-status"/> for further details.
+    </para>
+   </sect3>
+
+   <sect3 id="vacuum-xid-exhaustion">
+    <title>Transaction ID exhaustion</title>
+    <para>
+     If for some reason autovacuum fails to advance any table's
+     <structfield>relfrozenxid</structfield> for an extended period (during
+     which transaction IDs continue to be allocated), the system will begin to
+     emit warning messages once the database's oldest XIDs attain an age
+     within forty million transactions of the 2.1 billion XID hard limit
+     described in <xref linkend="vacuum-aggressive"/>.  For example:
 
 <programlisting>
 WARNING:  database "mydb" must be vacuumed within 39985967 transactions
 HINT:  To avoid a database shutdown, execute a database-wide VACUUM in that database.
 </programlisting>
 
-    (A manual <command>VACUUM</command> should fix the problem, as suggested by the
-    hint; but note that the <command>VACUUM</command> must be performed by a
-    superuser, else it will fail to process system catalogs and thus not
-    be able to advance the database's <structfield>datfrozenxid</structfield>.)
-    If these warnings are
-    ignored, the system will shut down and refuse to start any new
-    transactions once there are fewer than three million transactions left
-    until wraparound:
+     (A manual <command>VACUUM</command> should fix the problem, as suggested by the
+     hint; but note that the <command>VACUUM</command> must be performed by a
+     superuser, else it will fail to process system catalogs and thus not
+     be able to advance the database's <structfield>datfrozenxid</structfield>.)
+     If these warnings are ignored, the system will eventually refuse
+     to allocate new transaction IDs.  This happens at the point that
+     there are fewer than three million transactions left:
 
 <programlisting>
 ERROR:  database is not accepting commands to avoid wraparound data loss in database "mydb"
 HINT:  Stop the postmaster and vacuum that database in single-user mode.
 </programlisting>
 
-    The three-million-transaction safety margin exists to let the
-    administrator recover without data loss, by manually executing the
-    required <command>VACUUM</command> commands.  However, since the system will not
-    execute commands once it has gone into the safety shutdown mode,
-    the only way to do this is to stop the server and start the server in single-user
-    mode to execute <command>VACUUM</command>.  The shutdown mode is not enforced
-    in single-user mode.  See the <xref linkend="app-postgres"/> reference
-    page for details about using single-user mode.
-   </para>
-
-   <sect3 id="vacuum-for-multixact-wraparound">
-    <title>Multixacts and Wraparound</title>
-
-    <indexterm>
-     <primary>MultiXactId</primary>
-    </indexterm>
-
-    <indexterm>
-     <primary>wraparound</primary>
-     <secondary>of multixact IDs</secondary>
-    </indexterm>
-
-    <para>
-     <firstterm>Multixact IDs</firstterm> are used to support row locking by
-     multiple transactions.  Since there is only limited space in a tuple
-     header to store lock information, that information is encoded as
-     a <quote>multiple transaction ID</quote>, or multixact ID for short,
-     whenever there is more than one transaction concurrently locking a
-     row.  Information about which transaction IDs are included in any
-     particular multixact ID is stored separately in
-     the <filename>pg_multixact</filename> subdirectory, and only the multixact ID
-     appears in the <structfield>xmax</structfield> field in the tuple header.
-     Like transaction IDs, multixact IDs are implemented as a
-     32-bit counter and corresponding storage, all of which requires
-     careful aging management, storage cleanup, and wraparound handling.
-     There is a separate storage area which holds the list of members in
-     each multixact, which also uses a 32-bit counter and which must also
-     be managed.
+     The three-million-transaction safety margin exists to let the
+     administrator recover without data loss, by manually executing the
+     required <command>VACUUM</command> commands.  However, since the system will not
+     execute commands once it has gone into the safety shutdown mode,
+     the only way to do this is to stop the server and start the server in single-user
+     mode to execute <command>VACUUM</command>.  The shutdown mode is not enforced
+     in single-user mode.  See the <xref linkend="app-postgres"/> reference
+     page for details about using single-user mode.
     </para>
 
     <para>
-     Whenever <command>VACUUM</command> scans any part of a table, it will replace
-     any multixact ID it encounters which is older than
-     <xref linkend="guc-vacuum-multixact-freeze-min-age"/>
-     by a different value, which can be the zero value, a single
-     transaction ID, or a newer multixact ID.  For each table,
-     <structname>pg_class</structname>.<structfield>relminmxid</structfield> stores the oldest
-     possible multixact ID still appearing in any tuple of that table.
-     If this value is older than
-     <xref linkend="guc-vacuum-multixact-freeze-table-age"/>, an aggressive
-     vacuum is forced.  As discussed in the previous section, an aggressive
-     vacuum means that only those pages which are known to be all-frozen will
-     be skipped.  <function>mxid_age()</function> can be used on
-     <structname>pg_class</structname>.<structfield>relminmxid</structfield> to find its age.
+     A similar safety mechanism is used to prevent Multixact ID allocations
+     when any table's <structfield>relminmxid</structfield> is dangerously far
+     in the past: Multixact ID exhaustion.  If the system isn't experiencing
+     transaction ID exhaustion, Multixact ID exhaustion can be fixed
+     non-invasively by running a manual <command>VACUUM</command> without
+     entering single-user mode (otherwise follow the procedure for transaction
+     ID exhaustion).  See <xref linkend="monitoring-table-age"/> for details
+     on how to determine which table's <structfield>relminmxid</structfield>
+     is dangerously far in the past.
     </para>
 
-    <para>
-     Aggressive <command>VACUUM</command>s, regardless of what causes
-     them, are <emphasis>guaranteed</emphasis> to be able to advance
-     the table's <structfield>relminmxid</structfield>.
-     Eventually, as all tables in all databases are scanned and their
-     oldest multixact values are advanced, on-disk storage for older
-     multixacts can be removed.
-    </para>
-
-    <para>
-     As a safety device, an aggressive vacuum scan will
-     occur for any table whose multixact-age is greater than <xref
-     linkend="guc-autovacuum-multixact-freeze-max-age"/>.  Also, if the
-     storage occupied by multixacts members exceeds 2GB, aggressive vacuum
-     scans will occur more often for all tables, starting with those that
-     have the oldest multixact-age.  Both of these kinds of aggressive
-     scans will occur even if autovacuum is nominally disabled.
-    </para>
+    <note>
+     <para>
+      Autovacuum has two different mechanisms that are designed to avoid
+      transaction ID exhaustion. The first mechansim is anti-wraparound
+      autovacuuming.  There is an second, independent mechanism, used when
+      <structfield>relfrozenxid</structfield> and/or
+      <structfield>relminmxid</structfield> have already consumed a
+      significant fraction of the total available transaction ID space: the
+      failsafe.
+     </para>
+     <para>
+      The failsafe is triggered by <command>VACUUM</command> when the table's
+      <structfield>relfrozenxid</structfield> attains an age of <xref
+       linkend="guc-vacuum-failsafe-age"/> XIDs, or when the table's
+      <structfield>relminmxid</structfield> attains an age of <xref
+       linkend="guc-vacuum-multixact-failsafe-age"/> Multixact IDs.  This
+      happens dynamically, when the risk of eventual transaction ID (or
+      Multixact ID) exhaustion is deemed to outweigh the risks of not
+      proceeding as planned with ordinary vacuuming.
+     </para>
+     <para>
+      Once the failsafe triggers, <command>VACUUM</command> prioritizes
+      advancing <structfield>relfrozenxid</structfield> and/or
+      <structfield>relminmxid</structfield> to avoid transaction ID
+      exhaustion.  Most notably, <command>VACUUM</command> bypasses any
+      remaining non-essential maintenance, such as index vacuuming.
+     </para>
+    </note>
    </sect3>
   </sect2>
 
@@ -784,9 +1239,19 @@ HINT:  Stop the postmaster and vacuum that database in single-user mode.
     Vacuum maintains a <link linkend="storage-vm">visibility map</link> for each
     table to keep track of which pages contain only tuples that are known to be
     visible to all active transactions (and all future transactions, until the
-    page is again modified).  This has two purposes.  First, vacuum
-    itself can skip such pages on the next run, since there is nothing to
-    clean up.
+    page is again modified).  A separate bit tracks whether all of the tuples
+    are frozen.
+   </para>
+
+   <para>
+    The visibility map serves two purposes.
+   </para>
+
+   <para>
+    First, <command>VACUUM</command> itself can skip such pages on the
+    next run, since there is nothing to clean up.  Even <link
+     linkend="vacuum-aggressive">aggressive <command>VACUUM</command>s</link>
+    can skip pages that are both all-visible and all-frozen.
    </para>
 
    <para>
@@ -806,6 +1271,79 @@ HINT:  Stop the postmaster and vacuum that database in single-user mode.
    </para>
   </sect2>
 
+  <sect2 id="vacuum-truncate-xact-status">
+   <title>Truncating Transaction Status Information</title>
+
+   <para>
+    As discussed in <xref linkend="vacuum-aggressive"/>, aggressive
+    autovacuuming plays a critical role in maintaining the XID address space
+    for the system as a whole.  A secondary goal of this whole process is to
+    enable eventual truncation of the oldest transaction status information in
+    the <glossterm linkend="glossary-db-cluster">database cluster</glossterm>
+    as a whole.  This status information is stored in dedicated <link
+     linkend="monitoring-pg-stat-slru-view">simple least-recently-used</link>
+    (<acronym>SLRU</acronym>) caches backed by external storage (see <xref
+     linkend="storage-file-layout"/>).  Truncation is only possible when
+    <command>VACUUM</command> can ascertain that there are no references to
+    the oldest entries remaining in any table, from any database, by taking
+    the earliest <structfield>datfrozenxid</structfield> and
+    <structfield>datminmxid</structfield> among all databases in the cluster.
+    This isn't a maintenance task that affects individual tables; it's a
+    maintenance task that affects the whole cluster.
+   </para>
+
+   <para>
+    The space required to store transaction status information is likely to be
+    a low priority for most database administrators.  It may occasionally be
+    useful to limit the maximum storage overhead used for transaction status
+    information by making anti-wraparound autovacuums happen more frequently.
+    The frequency of system-wide anti-wraparound autovacuuming increases when
+    <varname>autovacuum_freeze_max_age</varname> and
+    <varname>autovacuum_multixact_freeze_max_age</varname> are decreased in
+    <filename>postgresql.conf</filename>.  This approach is effective (at
+    limiting the storage required for transaction status information) because
+    the <emphasis>oldest</emphasis> <structfield>datfrozenxid</structfield>
+    and <structfield>datminmxid</structfield> in the cluster are very likely
+    to depend on the frequency of anti-wraparound autovacuuming of completely
+    static tables.  See <xref linkend="vacuum-antiwraparound-autovacuums"/> for
+    further discussion of the role of anti-wraparound autovacuuming in
+    advancing the cluster-wide oldest unfrozen transaction ID.
+   </para>
+
+   <para>
+    There are two <acronym>SLRU</acronym> storage areas associated with
+    transaction IDs.  First, there is <filename>pg_xact</filename>, which
+    stores commit/abort status information.  Second, there is
+    <filename>pg_commit_ts</filename>, which stores transaction commit
+    timestamps (when <xref linkend="guc-track-commit-timestamp"/> is set to
+    <literal>on</literal>).  The default
+    <varname>autovacuum_freeze_max_age</varname> setting of 200 million
+    transactions translates to about 50MB of <filename>pg_xact</filename>
+    storage, and about 2GB of <filename>pg_commit_ts</filename> storage when
+    <varname>track_commit_timestamp</varname> is enabled (it is set to
+    <literal>off</literal> by default, which totally avoids the need to store
+    anything in <filename>pg_commit_ts</filename>).
+   </para>
+
+   <para>
+    There are also two <acronym>SLRU</acronym> storage areas associated with
+    Multixact IDs:  <filename>pg_multixact/members</filename>, and
+    <filename>pg_multixact/offsets</filename>.  These are logically one
+    storage area, implemented as two distinct storage areas.  There is no
+    simple formula to determine the storage overhead per Multixact ID, since
+    Multixact IDs have a variable number of member transaction IDs (this is
+    what necessitates using two different physical storage areas).  Note,
+    however, that if <filename>pg_multixact/members</filename> exceeds 2GB,
+    the effective value of <varname>autovacuum_multixact_freeze_max_age</varname>
+    used by autovacuum (and <command>VACUUM</command>) will be lower.  This
+    results in more frequent
+    <link linkend="vacuum-antiwraparound-autovacuums">anti-wraparound
+     autovacuums</link> (since that's the only approach that reliably limits
+    the size of these storage areas).  It might also increase the frequency of
+    aggressive <command>VACUUM</command>s more generally.
+   </para>
+  </sect2>
+
   <sect2 id="vacuum-for-statistics">
    <title>Updating Planner Statistics</title>
 
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 99f7f95c3..0888342ef 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1032,9 +1032,11 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
      <row>
       <entry><literal>BufferPin</literal></entry>
       <entry>The server process is waiting for exclusive access to
-       a data buffer.  Buffer pin waits can be protracted if
-       another process holds an open cursor that last read data from the
-       buffer in question. See <xref linkend="wait-event-bufferpin-table"/>.
+       a data buffer.  Buffer pin waits by aggressive
+       <command>VACUUM</command> (see <xref linkend="vacuum-aggressive"/>) can
+       be protracted if another process holds an for details on how to open
+       cursor that last read data from the buffer in question.
+       See <xref linkend="wait-event-bufferpin-table"/>.
       </entry>
      </row>
      <row>
@@ -6246,6 +6248,78 @@ FROM pg_stat_get_backend_idset() AS backendid;
  </sect2>
  </sect1>
 
+ <sect1 id="monitoring-table-age">
+  <title>Monitoring table age</title>
+
+  <para>
+   It is crucial that autovacuum is able to run <command>VACUUM</command> (or
+   that every table is manually vacuumed) at somewhat regular intervals.  Even
+   completely static tables need to participate in management of the
+   transaction ID space by <command>VACUUM</command>, as explained in <xref
+    linkend="vacuum-freezing-xid-space"/>.
+  </para>
+
+  <para>
+   This section provides details about how to monitor for this.  You might
+   want to use a third party monitoring and alterting tool for this.  Many
+   have off-the-shelf queries similar to the reference query shown here.
+  </para>
+
+  <caution>
+   <para>
+    Temporary tables are not maintained by autovacuum (see <xref
+     linkend="autovacuum-limitations"/>), but nevertheless have the same
+    requirements for freezing and <structfield>relfrozenxid</structfield>
+    advancement as permanent tables.
+   </para>
+   <para>
+    This means application code that <quote>leaks</quote> temporary tables can
+    threaten the availability of the system; eventually, the system will enter
+    a mode that makes it temporarily unable to allocate new transaction IDs
+    (see <xref linkend="vacuum-xid-exhaustion"/>), since autovacuum's usual
+    strategies for preventing that from happening cannot be used.
+   </para>
+  </caution>
+
+  <para>
+   A convenient way to examine information about
+   <structfield>relfrozenxid</structfield> and
+   <structfield>relminmxid</structfield> is to execute queries such as:
+
+<programlisting>
+SELECT c.oid::regclass as table_name,
+greatest(age(c.relfrozenxid),
+         age(t.relfrozenxid)) as xid_age,
+mxid_age(c.relminmxid)
+FROM pg_class c
+LEFT JOIN pg_class t ON c.reltoastrelid = t.oid
+WHERE c.relkind IN ('r', 'm');
+
+SELECT datname,
+age(datfrozenxid) as xid_age,
+mxid_age(datminmxid)
+FROM pg_database;
+</programlisting>
+
+   The <function>age</function> function returns the number of transactions
+   from <structfield>relfrozenxid</structfield> to the next unallocated
+   transaction ID.  The <function>mxid_age</function> function returns the
+   number of Multixact IDs from <structfield>relminmxid</structfield> to the
+   next unallocated Multixact ID.
+  </para>
+
+  <para>
+   The system should always have significant XID allocation slack capacity.
+   Ideally, the greatest
+   <literal>age(relfrozenxid)</literal>/<literal>age(datfrozenxid)</literal>
+   in the system will never be more than a fraction of the 2.1 billion XID
+   hard limit described in <xref linkend="vacuum-aggressive"/>.  The default
+   <varname>vacuum_freeze_table_age</varname> setting of 200 million
+   transactions implies that the system should never use significantly more
+   than about 10% of that hard limit.
+  </para>
+ </sect1>
+
  <sect1 id="monitoring-locks">
   <title>Viewing Locks</title>
 
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 10ef699fa..307504acb 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1514,11 +1514,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
      If true, the autovacuum daemon will perform automatic <command>VACUUM</command>
      and/or <command>ANALYZE</command> operations on this table following the rules
      discussed in <xref linkend="autovacuum"/>.
-     If false, this table will not be autovacuumed, except to prevent
-     transaction ID wraparound. See <xref linkend="vacuum-for-wraparound"/> for
-     more about wraparound prevention.
-     Note that the autovacuum daemon does not run at all (except to prevent
-     transaction ID wraparound) if the <xref linkend="guc-autovacuum"/>
+     If false, this table will not be autovacuumed, except to run
+     anti-wraparound autovacuum (see <xref linkend="vacuum-antiwraparound-autovacuums"/>).
+     Note that the autovacuum daemon does not run at all (except to run
+     anti-wraparound autovacuums) if the <xref linkend="guc-autovacuum"/>
      parameter is false; setting individual tables' storage parameters does
      not override that.  Therefore there is seldom much point in explicitly
      setting this storage parameter to <literal>true</literal>, only
diff --git a/doc/src/sgml/ref/prepare_transaction.sgml b/doc/src/sgml/ref/prepare_transaction.sgml
index f4f6118ac..719e0b25a 100644
--- a/doc/src/sgml/ref/prepare_transaction.sgml
+++ b/doc/src/sgml/ref/prepare_transaction.sgml
@@ -126,13 +126,13 @@ PREPARE TRANSACTION <replaceable class="parameter">transaction_id</replaceable>
    <para>
     It is unwise to leave transactions in the prepared state for a long time.
     This will interfere with the ability of <command>VACUUM</command> to reclaim
-    storage, and in extreme cases could cause the database to shut down
-    to prevent transaction ID wraparound (see <xref
-    linkend="vacuum-for-wraparound"/>).  Keep in mind also that the transaction
-    continues to hold whatever locks it held.  The intended usage of the
-    feature is that a prepared transaction will normally be committed or
-    rolled back as soon as an external transaction manager has verified that
-    other databases are also prepared to commit.
+    storage, and in extreme cases could cause the database to refuse to
+    allocate new transaction IDs (see <xref linkend="vacuum-xid-exhaustion"/>).
+    Keep in mind also that the transaction continues to hold whatever locks it
+    held.  The intended usage of the feature is that a prepared transaction
+    will normally be committed or rolled back as soon as an external
+    transaction manager has verified that other databases are also prepared to
+    commit.
    </para>
 
    <para>
diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml
index 57bc4c23e..f8c010123 100644
--- a/doc/src/sgml/ref/vacuum.sgml
+++ b/doc/src/sgml/ref/vacuum.sgml
@@ -123,7 +123,9 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ <replaceable class="paramet
     <term><literal>FREEZE</literal></term>
     <listitem>
      <para>
-      Selects aggressive <quote>freezing</quote> of tuples.
+      Makes <quote>freezing</quote> <emphasis>maximally</emphasis>
+      aggressive, and forces <command>VACUUM</command> to use its
+      <link linkend="vacuum-aggressive">aggressive strategy</link>.
       Specifying <literal>FREEZE</literal> is equivalent to performing
       <command>VACUUM</command> with the
       <xref linkend="guc-vacuum-freeze-min-age"/> and
@@ -218,11 +220,11 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ <replaceable class="paramet
       <emphasis>always</emphasis> skip index vacuuming, even when
       there are many dead tuples in the table.  This may be useful
       when it is necessary to make <command>VACUUM</command> run as
-      quickly as possible to avoid imminent transaction ID wraparound
-      (see <xref linkend="vacuum-for-wraparound"/>).  However, the
-      wraparound failsafe mechanism controlled by <xref
+      quickly as possible to avoid imminent transaction ID exhaustion
+      (see <xref linkend="vacuum-xid-exhaustion"/>).  However, the failsafe
+      mechanism controlled by <xref
        linkend="guc-vacuum-failsafe-age"/>  will generally trigger
-      automatically to avoid transaction ID wraparound failure, and
+      automatically to avoid transaction ID exhaustion failure, and
       should be preferred.  If index cleanup is not performed
       regularly, performance may suffer, because as the table is
       modified indexes will accumulate dead tuples and the table
@@ -232,7 +234,7 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ <replaceable class="paramet
      <para>
       This option has no effect for tables that have no index and is
       ignored if the <literal>FULL</literal> option is used.  It also
-      has no effect on the transaction ID wraparound failsafe
+      has no effect on the transaction ID exhaustion failsafe
       mechanism.  When triggered it will skip index vacuuming, even
       when <literal>INDEX_CLEANUP</literal> is set to
       <literal>ON</literal>.
diff --git a/doc/src/sgml/ref/vacuumdb.sgml b/doc/src/sgml/ref/vacuumdb.sgml
index da2393783..9621fe378 100644
--- a/doc/src/sgml/ref/vacuumdb.sgml
+++ b/doc/src/sgml/ref/vacuumdb.sgml
@@ -231,9 +231,11 @@ PostgreSQL documentation
        <para>
         Only execute the vacuum or analyze commands on tables with a multixact
         ID age of at least <replaceable class="parameter">mxid_age</replaceable>.
-        This setting is useful for prioritizing tables to process to prevent
-        multixact ID wraparound (see
-        <xref linkend="vacuum-for-multixact-wraparound"/>).
+        This setting is useful for prioritizing tables to advance
+        <structname>pg_class</structname>.<structfield>relfrozenxid</structfield>
+        for via an aggressive <command>VACUUM</command>
+        (see <xref linkend="vacuum-aggressive"/> and
+        <xref linkend="monitoring-table-age"/>).
        </para>
        <para>
         For the purposes of this option, the multixact ID age of a relation is
@@ -252,9 +254,12 @@ PostgreSQL documentation
        <para>
         Only execute the vacuum or analyze commands on tables with a
         transaction ID age of at least
-        <replaceable class="parameter">xid_age</replaceable>.  This setting
-        is useful for prioritizing tables to process to prevent transaction
-        ID wraparound (see <xref linkend="vacuum-for-wraparound"/>).
+        <replaceable class="parameter">xid_age</replaceable>.  This
+        setting is useful for prioritizing tables to advance
+        <structname>pg_class</structname>.<structfield>relminmxid</structfield>
+        for via an aggressive <command>VACUUM</command>
+        (see <xref linkend="vacuum-aggressive"/> and
+        <xref linkend="monitoring-table-age"/>).
        </para>
        <para>
         For the purposes of this option, the transaction ID age of a relation
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 148fb1b49..6d7ddc685 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -664,7 +664,8 @@ This information can also be used
 by <link linkend="indexes-index-only-scans"><firstterm>index-only
 scans</firstterm></link> to answer queries using only the index tuple.
 The second bit, if set, means that all tuples on the page have been frozen.
-That means that even an anti-wraparound vacuum need not revisit the page.
+That means that even an aggressive vacuum (see <xref linkend="vacuum-aggressive"/>)
+need not revisit the page.
 </para>
 
 <para>
diff --git a/doc/src/sgml/xact.sgml b/doc/src/sgml/xact.sgml
index 8a1f9fd6f..3cba050f0 100644
--- a/doc/src/sgml/xact.sgml
+++ b/doc/src/sgml/xact.sgml
@@ -180,7 +180,7 @@
    rows and can be inspected using the <xref linkend="pgrowlocks"/>
    extension.  Row-level read locks might also require the assignment
    of multixact IDs (<literal>mxid</literal>;  see <xref
-   linkend="vacuum-for-multixact-wraparound"/>).
+   linkend="vacuum-freezing-xid-space"/>).
   </para>
  </sect1>
 
-- 
2.40.1

