Understand transaction-ID aging and freezing, monitor relfrozenxid/datfrozenxid safely, and build a prevention runbook without manufacturing a wraparound emergency.

Transaction ID Wraparound, Freezing, relfrozenxid, and Emergency Prevention

Run a guided insert/update/delete/vacuum story with pageinspect and visibility diagnostics, correlate line pointers and tuple flags with SQL-visible state, and define the boundary between diagnostics and application interfaces.

Intermediate → Advanced160–210 minutesEvidence-driven maintenance labCurrent patched PostgreSQL 18.xCore PostgreSQL; supplied diagnostic extensions only where labeledLocal table owner/admin privileges as indicatedNo paid or managed-service dependencyLast reviewed: August 2026

Learning outcomes

Vacuum is also a correctness mechanism. PostgreSQL transaction identifiers (XIDs) are finite 32-bit values that are compared using wraparound-aware age semantics. If old tuple XIDs are never frozen, eventually very old data could appear to be “in the future.” PostgreSQL prevents this with freezing and anti-wraparound vacuum. The correct lesson is therefore prevention and monitoring—not manufacturing a cluster close to shutdown.

01

Explain XID aging and why PostgreSQL needs freezing despite using MVCC.

02

Interpret age(relfrozenxid) and age(datfrozenxid) as operational risk indicators rather than business timestamps.

03

Inspect freeze-related settings and anti-wraparound evidence safely.

04

Use VACUUM FREEZE on a disposable table to observe horizon movement without approaching dangerous ages.

05

Build a conceptual emergency runbook that prioritizes database availability and the oldest relations.

Safety boundary

This course will not lower wraparound safety settings to tiny values on a valuable cluster, generate billions of transactions, or disable protective vacuum. The goal is to learn the signals and response before an emergency exists.

1. What does “age” mean for an XID?

Normal XIDs are compared in a circular 32-bit space. PostgreSQL interprets roughly half the space as past and half as future relative to the current transaction horizon. Freezing replaces the need to keep consulting an ancient creating transaction's status: frozen tuples are treated as older than every ordinary running transaction for visibility purposes.

sql · inspect database and table ages
SELECT datname,       age(datfrozenxid) AS xid_age,       datfrozenxidFROM pg_databaseWHERE datallowconnORDER BY xid_age DESC;SELECT c.oid::regclass AS relation,       age(c.relfrozenxid) AS xid_age,       c.relfrozenxidFROM pg_class AS cJOIN pg_namespace AS n ON n.oid = c.relnamespaceWHERE n.nspname = 'app'  AND c.relkind IN ('r','m')ORDER BY xid_age DESCLIMIT 20;

relfrozenxid summarizes the oldest normal XID that may still need consideration for a relation. datfrozenxid tracks the database-level oldest horizon derived from its relations. These are maintenance metadata, not timestamps and not identifiers to expose to applications.

2. Read the cluster's actual freeze policy

sql · freeze and failsafe settings
SELECT name, setting, unit, context, sourceFROM pg_settingsWHERE name IN (  'autovacuum_freeze_max_age',  'vacuum_freeze_min_age',  'vacuum_freeze_table_age',  'vacuum_failsafe_age')ORDER BY name;

autovacuum_freeze_max_age is a major prevention boundary: the server schedules anti-wraparound vacuum before relations become dangerously old. The other horizons influence when eligible tuples are frozen and when more aggressive/failsafe behavior is justified. Treat the live settings as the source of truth; do not hard-code a percentage from a blog without understanding the configured values and transaction consumption rate.

sql · record current transaction context without consuming a storm of XIDs
SELECT pg_current_xact_id_if_assigned() AS current_xid_if_assigned;SELECT datname, age(datfrozenxid) AS xid_ageFROM pg_databaseWHERE datname = current_database();

Read-only statements need not assign a normal XID immediately, which is one reason a safe monitoring lab does not try to “count transactions” by forcing transaction identifiers. Observe age metadata directly.

3. A safe freeze laboratory

sql · create a small relation and record its age
DROP TABLE IF EXISTS app.ch09_freeze_lab;CREATE TABLE app.ch09_freeze_lab (    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,    note text NOT NULL);INSERT INTO app.ch09_freeze_lab(note)SELECT 'freeze-demo-' || gFROM generate_series(1, 5000) AS g;SELECT age(relfrozenxid) AS before_age, relfrozenxidFROM pg_classWHERE oid = 'app.ch09_freeze_lab'::regclass;
sql · vacuum freeze and re-observe
VACUUM (FREEZE, VERBOSE, ANALYZE) app.ch09_freeze_lab;SELECT age(relfrozenxid) AS after_age, relfrozenxidFROM pg_classWHERE oid = 'app.ch09_freeze_lab'::regclass;

On a freshly created table, both ages are already small, so do not expect a dramatic numeric demonstration. The important evidence is that VACUUM FREEZE performs an aggressive freeze-oriented pass and advances relation metadata when there is eligible work. In production, routine autovacuum should prevent the need to run this blindly everywhere.

4. Anti-wraparound vacuum is special

Disabling ordinary autovacuum on a table does not remove PostgreSQL's responsibility to prevent transaction-ID wraparound. Protective vacuum can still be required. That is why “turn autovacuum off because it is causing I/O” is not a complete operational strategy.

sql · find oldest relations and maintenance history
SELECT c.oid::regclass AS relation,       age(c.relfrozenxid) AS xid_age,       s.last_autovacuum,       s.autovacuum_count,       s.n_dead_tupFROM pg_class AS cJOIN pg_namespace AS n ON n.oid = c.relnamespaceLEFT JOIN pg_stat_all_tables AS s ON s.relid = c.oidWHERE c.relkind IN ('r','m')  AND n.nspname NOT IN ('pg_catalog','information_schema')ORDER BY xid_age DESCLIMIT 30;

For incident triage, sort by age first. A large table with a high age is more urgent than a small table merely because its dead-tuple estimate is larger.

sql · check transactions that can hold horizons open
SELECT pid, usename, state, xact_start, backend_xmin, queryFROM pg_stat_activityWHERE backend_xmin IS NOT NULL   OR xact_start IS NOT NULLORDER BY xact_start NULLS LAST;SELECT gid, prepared, owner, databaseFROM pg_prepared_xactsORDER BY prepared;

Long-running transactions and prepared transactions are not equivalent to a high relfrozenxid, but they can hold visibility horizons and complicate cleanup. Investigate them as part of the same maintenance incident rather than vacuuming blindly.

5. Build an emergency-prevention dashboard

sql · risk ratio against configured maximum
WITH maxage AS (  SELECT current_setting('autovacuum_freeze_max_age')::numeric AS max_age)SELECT d.datname,       age(d.datfrozenxid) AS xid_age,       round(100 * age(d.datfrozenxid)::numeric / maxage.max_age, 2) AS pct_of_configured_maxFROM pg_database AS dCROSS JOIN maxageWHERE d.datallowconnORDER BY xid_age DESC;

This percentage is a triage aid, not a universal alert threshold. Your alerting policy should also consider transaction consumption velocity, vacuum runtime on the largest old tables, worker/I/O capacity, replication/backup constraints, and how quickly an operator can intervene.

Do not manufacture the emergency

A training environment does not need a near-wraparound XID age. The safe exercise is to inspect configuration, rank ages, run a harmless FREEZE on disposable data, and rehearse the decision sequence.

6. Production judgment: age is a trajectory, not a one-time number

A healthy operating model alerts early enough that ordinary anti-wraparound autovacuum has time to finish the largest old relations. Track both absolute age and its growth rate. A high but falling age after successful vacuum is a different incident from a lower age that is accelerating while workers are blocked. Also monitor MultiXact age in real production systems, because row-locking workloads have a separate wraparound domain even though this lesson focuses on normal XIDs.

Do not “solve” XID pressure by terminating arbitrary sessions, disabling replication safeguards, or globally forcing FREEZE without understanding consequences. Preserve business correctness, identify the oldest maintenance horizons, and give protective vacuum enough resources to make measurable forward progress.

7. Conceptual emergency runbook

If ages become unexpectedly high: confirm the actual oldest databases/relations; check whether autovacuum is running and whether long-running transactions/prepared transactions/replication slots are obstructing cleanup; protect I/O and worker capacity for anti-wraparound work; vacuum the oldest relations deliberately; avoid schema rewrites or unrelated heavy maintenance; and continue monitoring age until the trajectory reverses. If the server begins refusing commands to prevent wraparound, follow the official major-version recovery procedure rather than improvising.

sql · cleanup
DROP TABLE IF EXISTS app.ch09_freeze_lab;

Check your understanding

  1. Why is XID freezing a correctness requirement rather than merely a space optimization?
  2. What does age(relfrozenxid) summarize?
  3. Why is a fixed alert percentage insufficient without transaction-consumption velocity?
  4. Does table-level autovacuum disabling mean wraparound protection is gone?
  5. Why does this course avoid lowering freeze limits just to create a dramatic demo?
Review the answers

Freezing prevents old tuple XIDs from becoming ambiguous as the 32-bit XID space cycles. relfrozenxid summarizes the oldest normal XID horizon relevant to a relation. Risk depends on how fast XIDs are being consumed and how long cleanup will take, not only a percentage. PostgreSQL still performs necessary wraparound-prevention maintenance. Artificially creating a near-emergency teaches unsafe habits and can make a cluster unavailable.

Authoritative references

These mechanisms are version-sensitive. Use the documentation for the PostgreSQL major you actually operate.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.