PostgreSQL: database is not accepting commands to avoid wraparound data loss
ERROR: database is not accepting commands to avoid wraparound data loss in database "appdb"
HINT: Stop the postmaster and vacuum that database in single-user mode.
This is the emergency brake of PostgreSQL MVCC — and if you are reading this during the emergency: the fix is to remove whatever is blocking vacuum and then let VACUUM freeze the oldest tables. Note that the classic HINT about single-user mode is widely considered outdated advice on modern versions; a normal VACUUM usually does the job better and more safely.
What this error means
Transaction IDs (XIDs) are 32-bit values assigned from a counter that wraps around in a circle. For row visibility to stay correct, no live row may be older than about 2 billion transactions — so VACUUM continuously freezes old rows, marking them visible-forever and reclaiming their age. If freezing falls far enough behind, PostgreSQL first warns (database "appdb" must be vacuumed within N transactions), then stops accepting data-modifying commands entirely, a safety margin before actual wraparound would corrupt visibility.
The critical insight: this is virtually never "vacuum was too slow" alone — something was holding the freeze horizon back. Find it first; a VACUUM racing against an immovable horizon achieves nothing.
Common causes
- A very old open transaction pinning the horizon (a session
idle in transactionfor days or weeks). - Orphaned prepared transactions — created by two-phase commit, forgotten forever, invisible to casual inspection.
- Stale replication slots (with feedback) holding the horizon on behalf of a replica that no longer exists.
- Autovacuum disabled or chronically starved on huge tables.
How to diagnose it
-- Quanto è vecchio il database / le tabelle peggiori:
SELECT datname, age(datfrozenxid) FROM pg_database ORDER BY 2 DESC;
SELECT relname, age(relfrozenxid) FROM pg_class
WHERE relkind = 'r' ORDER BY 2 DESC LIMIT 20;
-- I tre soliti colpevoli dell'orizzonte bloccato:
SELECT pid, now() - xact_start AS age, state, query
FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_start LIMIT 5;
SELECT * FROM pg_prepared_xacts;
SELECT slot_name, active, xmin FROM pg_replication_slots;
How to fix it
- Remove the horizon blockers: terminate the ancient transactions,
COMMIT PREPARED/ROLLBACK PREPAREDthe orphaned two-phase transactions, drop dead replication slots. - Vacuum the worst offenders first: run
VACUUM (VERBOSE)on the tables with the highestrelfrozenxidage (or database-wide if time allows). With the blockers gone, ages drop and the database resumes accepting writes once back under the safety threshold. - Afterwards, make it structural: monitor
age(datfrozenxid)with alerts far below the 2-billion ceiling, keep autovacuum on and adequately resourced, and alert on prepared transactions and inactive slots — the silent horizon-holders.
