PostgreSQL is warning that autovacuum has fallen behind in freezing old transaction IDs. This warning appears when the database is approaching transaction ID wraparound. You must run VACUUM soon to freeze old rows and prevent data corruption.
This warning indicates that PostgreSQL's autovacuum process has not been able to freeze transaction IDs fast enough to keep pace with your database's transaction rate. PostgreSQL uses 32-bit transaction IDs (XIDs) which wrap around after approximately 4.3 billion transactions. To prevent catastrophic data loss from wraparound, PostgreSQL must regularly "freeze" old rows by marking them as permanently visible, independent of transaction IDs. When autovacuum falls behind this maintenance task, PostgreSQL emits this warning as a safety alert. The "N transactions" value tells you how many more transactions can be executed before PostgreSQL will shut down and refuse all write commands to prevent data corruption. This is an escalating warning that demands action: first comes this warning at 40 million transactions remaining, then critical warnings at smaller thresholds, and finally a complete write-lock if the situation isn't resolved.
Connect to the database and determine how close you are to wraparound:
SELECT datname, age(datfrozenxid) AS xid_age,
2147483647 - age(datfrozenxid) AS txids_remaining,
CASE
WHEN 2147483647 - age(datfrozenxid) < 3000000 THEN 'CRITICAL - VACUUM NOW'
WHEN 2147483647 - age(datfrozenxid) < 10000000 THEN 'DANGER - VACUUM SOON'
WHEN 2147483647 - age(datfrozenxid) < 40000000 THEN 'WARNING - VACUUM TODAY'
ELSE 'MONITOR'
END AS status
FROM pg_database
WHERE datname = current_database();If txids_remaining is less than 10 million, this is urgent. If less than 3 million, your database may enter read-only mode at any moment.
Old transactions prevent autovacuum from advancing the frozen transaction ID. Find them:
SELECT pid, usename, state, query, now() - xact_start AS duration
FROM pg_stat_activity
WHERE state IS NOT NULL
AND xact_start IS NOT NULL
ORDER BY xact_start ASC
LIMIT 10;Look for idle transactions that have been open for hours or days. Terminate them:
-- Gentle termination
SELECT pg_cancel_backend(pid) FROM pg_stat_activity
WHERE pid != pg_backend_pid()
AND now() - xact_start > interval '30 minutes'
AND state = 'idle in transaction';
-- Force termination if needed
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE pid != pg_backend_pid()
AND now() - xact_start > interval '2 hours';Do not terminate system processes or your current connection.
Once old transactions are cleared, vacuum the database as a superuser. Start with a standard VACUUM:
VACUUM ANALYZE;For faster completion on PostgreSQL 12+, use optimized options:
VACUUM (INDEX_CLEANUP OFF, TRUNCATE OFF);Or use the command-line utility for all databases:
# Vacuum all databases with parallel workers
vacuumdb --all --jobs 4 --verbose
# Or specific database
vacuumdb --dbname yourdb --jobs 4 --analyzeDo NOT use VACUUM FULL - it will be slower and consume more disk space.
On PostgreSQL 13+, monitor real-time progress:
SELECT p.pid, p.datname, p.relid::regclass AS table_name,
p.phase, p.heap_blks_scanned, p.heap_blks_total,
round(100.0 * p.heap_blks_scanned / NULLIF(p.heap_blks_total, 0), 1) AS percent_done
FROM pg_stat_progress_vacuum p;After VACUUM completes, verify that transaction ID age has dropped significantly:
SELECT datname, age(datfrozenxid) AS xid_age,
2147483647 - age(datfrozenxid) AS txids_remaining
FROM pg_database
WHERE datname = current_database();The xid_age should now be in the millions (not hundreds of millions), and warnings should stop appearing.
Prevent this from recurring by tuning autovacuum parameters:
-- Lower freeze_max_age to trigger freezing more frequently
ALTER SYSTEM SET autovacuum_freeze_max_age = 100000000; -- Default is 200 million
-- Increase autovacuum workers for parallel vacuuming
ALTER SYSTEM SET autovacuum_max_workers = 4; -- Default is 3
-- Reduce vacuum cost delays if autovacuum is throttled
ALTER SYSTEM SET autovacuum_vacuum_cost_delay = 5; -- Default is 20ms
-- Increase available vacuum cost
ALTER SYSTEM SET autovacuum_vacuum_cost_limit = 1000; -- Default is -1 (unlimited)
-- Reload configuration
SELECT pg_reload_conf();For high-transaction-volume tables, set table-specific autovacuum settings:
ALTER TABLE high_volume_table
SET (autovacuum_freeze_min_age = 5000000,
autovacuum_freeze_table_age = 50000000);Set up monitoring to catch future issues before they escalate:
-- Create a monitoring view
CREATE OR REPLACE VIEW wraparound_status AS
SELECT datname, age(datfrozenxid) AS xid_age,
2147483647 - age(datfrozenxid) AS txids_remaining,
CASE
WHEN 2147483647 - age(datfrozenxid) < 3000000 THEN 'CRITICAL'
WHEN 2147483647 - age(datfrozenxid) < 10000000 THEN 'DANGER'
WHEN 2147483647 - age(datfrozenxid) < 40000000 THEN 'WARNING'
ELSE 'OK'
END AS status
FROM pg_database
ORDER BY xid_age DESC;
-- Query it regularly
SELECT * FROM wraparound_status;Alert when status transitions to DANGER or CRITICAL. Use external monitoring tools (DataGrip, pgAdmin, Datadog, New Relic) to track age(datfrozenxid) over time.
Why This Warning Matters: The warning threshold (40 million transactions remaining) is deliberately conservative. PostgreSQL has multiple escalation points: warning at 40M → critical warning at 10M → emergency shutdown at <3M. This gives you time to respond, but ignoring warnings will eventually force an outage.
Multixact ID Wraparound: PostgreSQL also tracks multixact IDs (MXIDs) for row-level locks. Similar warnings and thresholds apply to MXIDs. Check with age(datminmxid) and monitor both XID and MXID age separately.
Failsafe Mode (PostgreSQL 14+): PostgreSQL 14 introduced vacuum_failsafe_age (default 1.6 billion). When this threshold is crossed, autovacuum runs with extremely aggressive priority and cannot be cancelled, causing potential performance degradation. Prevention is much better than triggering failsafe mode.
Connection Poolers and Freezing: If you use PgBouncer or other connection poolers in session mode, they may prevent autovacuum from advancing the frozen transaction ID because pooler connections hold very old backend_xmin values. Consider switching to transaction mode or ensuring the pooler doesn't interfere with autovacuum maintenance.
Per-Table Analysis: If one table is significantly older than others, check if it has an unusually high autovacuum_freeze_min_age setting:
SELECT c.oid::regclass, a.attoptions
FROM pg_class c
JOIN pg_attribute a ON c.oid = a.attrelid
WHERE c.relname = 'problem_table';Incremental Freezing: On PostgreSQL 14+, VACUUM can freeze rows incrementally without scanning the entire table. Set autovacuum_freeze_min_age lower to trigger more frequent partial freezing, reducing the load of full-table freezes when you reach critical thresholds.
Cloud Database Services: On AWS RDS, Google Cloud SQL, or Azure Database for PostgreSQL, check the managed service's autovacuum settings. Some restrict autovacuum configuration, so you may need to contact support or scale to a larger instance to increase autovacuum capacity.
insufficient columns in unique constraint for partition key
How to fix "insufficient columns in unique constraint for partition key" in PostgreSQL
ERROR 42501: must be owner of table
How to fix "must be owner of table" in PostgreSQL
trigger cannot change partition destination
How to fix "Trigger cannot change partition destination" in PostgreSQL
SSL error: certificate does not match host name
SSL error: certificate does not match host name in PostgreSQL
No SSL connection
No SSL connection to PostgreSQL