Skip to main content
RunBook Academy

PostgreSQLVIII · VACUUM, Autovacuum and WraparoundVacuum

Transaction ID wraparound and anti-wraparound vacuum

Advanced⏱ ~35 minpsql

What you'll learn

  • Explain why a finite transaction id space requires freezing
  • Recognise an anti-wraparound vacuum in the log and in pg_stat_activity
  • Quantify the WAL cost of freezing before scheduling it
  • Respond correctly to a cluster approaching the limit, including what not to do

Prerequisites

Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27

Not yet marked complete on this device.

This is the failure that stops a PostgreSQL cluster accepting writes. It is fully preventable, it announces itself for weeks, and it still takes production systems down — because the warnings arrive on a database nobody was watching, or on a table nobody knew existed.

Why freezing is necessary

Transaction ids are 32 bits. About four billion values, and they wrap.

Visibility, from lesson VII-03, works by comparing a tuple’s xmin against the current transaction id: older means potentially visible, newer means not. That comparison is done in a circular space, where any id is “in the past” for half the range and “in the future” for the other half.

So a row written two billion transactions ago would, without intervention, become invisible — not deleted, not corrupted, simply unreadable, because its xmin has drifted into what now looks like the future.

Freezing is the fix. A frozen tuple is marked as visible to everything, permanently, and its xmin is no longer compared against anything. Vacuum does this, and relfrozenxid records how far back the guarantee extends for each relation.

The escalation ladder

Four thresholds, and the behaviour changes at each.

AgeSettingWhat happens
50,000,000vacuum_freeze_min_ageA vacuum that visits a page freezes tuples older than this
150,000,000vacuum_freeze_table_ageThe next vacuum on the table scans it in full rather than skipping all-visible pages
200,000,000autovacuum_freeze_max_ageAutovacuum forces an anti-wraparound vacuum regardless of anything else
1,600,000,000vacuum_failsafe_ageVacuum abandons throttling and skips index cleanup
~2,000,000,000The server refuses new transactions

The margin between 200 million and the shutdown point is roughly 1.8 billion transactions. On a cluster consuming a thousand transactions per second that is about three weeks.

Wraparound outages are never caused by a tight margin. They are caused by three weeks of a warning nobody read.

What an anti-wraparound vacuum looks like

This was reproduced on a test cluster by lowering autovacuum_freeze_max_age to 100,000 and restarting — waiting for 200 million real transactions is not practical, and the log output is identical.

Service impact possiblethe log line to recognise
$ docker logs rbpg-stor 2>&1 | grep "to prevent wraparound"
LOG:  automatic aggressive vacuum to prevent wraparound of table "postgres.public.narrow": index scans: 0
pages: 0 removed, 443 remain, 443 scanned (100.00% of total), 0 eagerly scanned
tuples: 0 removed, 100000 remain, 0 are dead but not yet removable
removable cutoff: 683715, which was 0 XIDs old when operation ended
new relfrozenxid: 683715, which is 682961 XIDs ahead of previous value
frozen: 443 pages from table (100.00% of total) had 100000 tuples frozen
visibility map: 0 pages set all-visible, 443 pages set all-frozen (443 were all-visible)
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed
avg read rate: 30.324 MB/s, avg write rate: 29.789 MB/s
buffer usage: 480 hits, 453 reads, 445 dirtied
WAL usage: 887 records, 445 full page images, 3676020 bytes, 0 buffers full
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.11 s

The phrase is automatic aggressive vacuum to prevent wraparound of table. Put it in your log alerting.

Read tuples: 0 removed, 100000 remain. This vacuum deleted nothing. It existed purely to freeze, and it rewrote every page of the table to do it.

Every database’s age fell to zero after the pass, including template0 and template1, which were vacuumed in the same sweep:

LOG:  automatic aggressive vacuum to prevent wraparound of table
      "template1.pg_toast.pg_toast_3600": index scans: 0

The cost of freezing, measured

What anti-wraparound vacuum does differently

Three behaviours that ordinary autovacuum does not have.

It ignores autovacuum_enabled = false. A table with autovacuum disabled is still force-vacuumed at autovacuum_freeze_max_age. This is the backstop that prevents that setting from being fatal.

It does not yield its lock. Ordinary autovacuum cancels itself when a session requests a conflicting lock. This one does not. A DROP TABLE or ALTER TABLE behind it waits, and everything behind that waits too.

It scans the whole table. It cannot skip all-visible pages, because freezing is exactly what those pages need.

The response, in order

When a database’s age is climbing toward the threshold:

1. Find the relation. Include TOAST relations and shared catalogues.

SELECT c.oid::regclass AS relation, c.relkind,
       age(c.relfrozenxid) AS xid_age,
       pg_size_pretty(pg_total_relation_size(c.oid)) AS size
  FROM pg_class c
 WHERE c.relkind IN ('r','m','t')
 ORDER BY age(c.relfrozenxid) DESC
 LIMIT 20;

2. Determine why it is not being frozen. This is one of the six modes from lesson VIII-04, and mode 1 — a held horizon — is the most common. Freezing cannot advance past the oldest snapshot either.

3. Remove the obstacle. End the transaction, drop the slot, stop the DDL.

4. Freeze deliberately, in a window.

-- one table, supervised, with a cost delay so it does not saturate storage
SET vacuum_cost_delay = '2ms';
VACUUM (FREEZE, VERBOSE) big_table;

vacuumdb --freeze --jobs=N --analyze handles a whole database with parallelism, which is the practical tool when several relations are behind.

5. Verify. age(relfrozenxid) should fall to near zero for that relation, and age(datfrozenxid) for the database should follow once the minimum across all its relations moves.

What to take from this

  • Transaction ids are finite and wrap; freezing makes old rows permanently visible.
  • The margin from forced vacuuming to shutdown is roughly 1.8 billion transactions — weeks, not hours.
  • automatic aggressive vacuum to prevent wraparound of table is the log string to alert on.
  • Freezing costs roughly the table’s size in WAL, in a burst.
  • Anti-wraparound vacuum ignores the per-table disable and does not yield. Do not kill it without a plan to replace it.
  • pg_resetwal is never the answer to a warning. Single-user mode is the answer to an actual shutdown.
  • Track multixact age alongside transaction age.

Cross-course references

  • Observability for Production Sysadmins — Part XIII (Rates and counters) covers projecting a deadline from an age series, and Part XVIII (Alerting rules) covers alerting on it early enough that the fix is a maintenance window rather than an outage.
  • Linux for Production Sysadmins — Part LXXXI (Incident command) covers running the response to a cluster that has stopped accepting writes.

Quiz

Knowledge check · 6 questions

  1. Q1. A 2 TB table has never been frozen and is about to receive its first anti-wraparound vacuum. Beyond the lock it will hold, what should be planned for?

  2. Q2. During an incident, an autovacuum worker is blocking a deployment. Its entry in pg_stat_activity shows a query containing '(to prevent wraparound)'. What is the correct response?

  3. Q3. A cluster with a heavily foreign-keyed schema and high concurrency begins issuing aggressive vacuums, yet age(datfrozenxid) across all databases is under ten million. What is the likely explanation?

  4. Q4. Which behaviours distinguish an anti-wraparound vacuum from an ordinary autovacuum? Select all that apply.

  5. Q5. pg_resetwal is an appropriate emergency tool when a cluster is approaching the transaction id limit and the aggressive vacuum is taking too long.

  6. Q6. Explain why freezing is necessary at all, in terms of how visibility is decided.

Passing score: 75%. Answers are checked in this browser.