This parameter is the last line of defense against PostgreSQL’s most famous failure mode. To explain what it does, a brief detour into how PostgreSQL knows which rows you are allowed to see.

A short tour of transaction IDs and freezing

Every transaction in PostgreSQL is assigned a transaction ID — an xid — when it first writes to the database. Every tuple on disk carries two of them: xmin, the xid of the transaction that created the tuple, and xmax, the xid of the transaction that deleted it (zero if not deleted). When you run a query, PostgreSQL consults your snapshot — a structured “what was committed when I started” — and decides whether to show you each tuple by comparing the snapshot to the xmin and xmax. This is MVCC, and it is how readers don’t block writers.

The problem: xid is a 32-bit unsigned integer. Two billion values, then it wraps. PostgreSQL handles wraparound by treating xids as living on a circle — any given xid has roughly two billion xids “in the past” and two billion “in the future” relative to the current one. As long as no live tuple has an xmin more than two billion xids old, this works.

Enter freezing. When vacuum encounters a tuple whose xmin is “old enough” — and definitely visible to every possible current and future transaction — it marks the tuple as frozen. A frozen tuple is, by convention, visible to everyone forever; the original xmin is no longer consulted, and the slot in xid-space it once occupied can be safely reused. (Mechanically, modern PostgreSQL uses a hint bit on the tuple header rather than literally rewriting xmin to a sentinel value, but the effect is the same.) Vacuum’s most critical job is not reclaiming dead-tuple space; it is freezing live tuples before their xids wrap.

What this parameter does

autovacuum_freeze_max_age is the maximum age, in transactions, that any table’s oldest unfrozen xid is allowed to reach before autovacuum is forced to launch an anti-wraparound vacuum against it. Default is 200,000,000. Minimum is 100,000. Maximum is 2,000,000,000. Context is postmaster, which means changing it requires a server restart. You cannot raise this in response to a developing wraparound situation.

The mechanic: autovacuum periodically inspects pg_class.relfrozenxid for every table. When age(relfrozenxid) exceeds autovacuum_freeze_max_age, the table is scheduled for a vacuum that will not be skipped, will not be cancelled by lock conflicts in the usual way, and will run even if autovacuum = off. The log will say “to prevent wraparound.” Anyone who has seen this message in production remembers it.

The two-billion ceiling is not arbitrary — it is the maximum theoretical xid distance, beyond which PostgreSQL refuses new write transactions to protect itself. The default of 200M leaves an order of magnitude of headroom, which sounds generous and is not, on a busy database. A system burning a thousand xids per second consumes the default in roughly 55 hours. Anti-wraparound vacuums are how you stay ahead of that, and autovacuum_freeze_max_age is the dial that decides when they start.

Tuning

Two reasons you might change it:

  • Lower it (say, to 100M) on systems with very high transaction rates, to spread the freezing work out and reduce the chance of multiple tables hitting wraparound vacuum simultaneously. PostgreSQL 16 introduced more eager opportunistic freezing during normal vacuums, which has reduced the urgency of this tuning, but it is still useful.
  • Raise it (toward, but never close to, two billion) on systems where you are deliberately delaying freezing work to a maintenance window. This is rare and usually wrong; the savings are small and the risk surface is enormous.

vacuum_freeze_min_age and vacuum_freeze_table_age are the closely related knobs that govern how aggressively normal vacuums freeze tuples; both get their own posts in this series, and tuning them is usually the better lever than touching this one.

Recommendation: Leave it at the default. Monitor age(datfrozenxid) per database and age(relfrozenxid) per table — there are several good queries floating around for this — and alert when any table crosses 50% of autovacuum_freeze_max_age. The parameter is the safety net; your job is to never need it.