age(datminmxid) in pg_database is growing — multixact IDs are approaching their 32-bit wraparound limit. This is a separate counter from transaction ID wraparound and requires distinct monitoring.
Diagnose it
SELECT datname,
age(datminmxid) AS mxid_age,
2147483647 - age(datminmxid) AS mxids_remaining,
datminmxid
FROM pg_database
ORDER BY mxid_age DESC;
-- Per-table multixact age:
SELECT n.nspname || '.' || c.relname AS table_name,
age(c.relminmxid) AS mxid_age
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
AND age(c.relminmxid) > 1000000000
ORDER BY mxid_age DESC;
Why it happens
Multixact IDs (MXIDs) are used internally when multiple transactions lock the same row
simultaneously (e.g., SELECT ... FOR SHARE from concurrent sessions). Each unique
combination of concurrent row-lockers creates a new MXID. Like transaction IDs, MXIDs have a
32-bit counter that wraps around. The fix is the same as XID wraparound: VACUUM must freeze
old MXID values. Workloads with heavy row-level locking exhaust MXIDs far faster than
transaction IDs.
How to fix it
-- Force multixact freezing:
SET vacuum_cost_delay = 0;
VACUUM FREEZE VERBOSE schema.tablename;
-- Or database-wide:
VACUUM FREEZE;
Prevent it next time
Monitor age(datminmxid) alongside age(datfrozenxid). Tune
autovacuum_multixact_freeze_max_age to be significantly lower than 2B. In
workloads with heavy SELECT FOR SHARE or foreign-key locks, the MXID counter
can grow much faster than the transaction counter — do not assume XID monitoring is sufficient.