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

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

  1. Remove the horizon blockers: terminate the ancient transactions, COMMIT PREPARED / ROLLBACK PREPARED the orphaned two-phase transactions, drop dead replication slots.
  2. Vacuum the worst offenders first: run VACUUM (VERBOSE) on the tables with the highest relfrozenxid age (or database-wide if time allows). With the blockers gone, ages drop and the database resumes accepting writes once back under the safety threshold.
  3. 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.
🔍 Essential background: VACUUM, autovacuum and bloat — the freezing mechanism and how autovacuum schedules itself.