The age(datfrozenxid) value for one or more databases is approaching the 2-billion-transaction safety threshold — failure to freeze will eventually cause PostgreSQL to enter read-only mode to prevent data corruption.
Diagnose it
-- XID age by database (run as superuser on each database cluster):
SELECT datname,
age(datfrozenxid) AS xid_age,
2147483647 - age(datfrozenxid) AS xids_remaining,
datfrozenxid
FROM pg_database
ORDER BY xid_age DESC;
-- Per-table view (on the specific database):
SELECT n.nspname || '.' || c.relname AS table_name,
age(c.relfrozenxid) AS table_xid_age,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
AND age(c.relfrozenxid) > 1500000000
ORDER BY table_xid_age DESC;
Guidance thresholds: alert at 1.5B (ages above this trigger PostgreSQL warnings
in the log); emergency VACUUM at 1.9B; PostgreSQL forces shutdown at 2.1B to prevent
data loss.
Why it happens
PostgreSQL transaction IDs are 32-bit unsigned integers. After 2 billion transactions, they
wrap around. To prevent this from causing data corruption, VACUUM must periodically
freeze old tuple versions, replacing their real XID with a special frozen XID that
is always considered visible. The datfrozenxid in pg_database
records the oldest non-frozen XID in the database.