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.

Este es el freno de emergencia del MVCC de PostgreSQL — y si estás leyendo esto durante la emergencia: la solución es eliminar lo que sea que esté bloqueando el vacuum y luego dejar que VACUUM congele (freeze) las tablas más viejas. Ten en cuenta que el clásico HINT sobre el modo single-user se considera ampliamente un consejo obsoleto en las versiones modernas; un VACUUM normal suele hacer el trabajo mejor y de forma más segura.

Qué significa este error

Los IDs de transacción (XID) son valores de 32 bits asignados desde un contador que da la vuelta en círculo. Para que la visibilidad de filas siga siendo correcta, ninguna fila viva puede ser más antigua que unos 2 mil millones de transacciones — así que VACUUM congela continuamente las filas viejas, marcándolas como visibles-para-siempre y recuperando su antigüedad. Si el freezing se queda lo bastante atrás, PostgreSQL primero avisa (database "appdb" must be vacuumed within N transactions), y luego deja de aceptar comandos que modifican datos por completo, un margen de seguridad antes de que el wraparound real corrompiera la visibilidad.

La idea crítica: esto casi nunca es "el vacuum fue demasiado lento" por sí solo — algo estaba reteniendo el horizonte de freeze. Encuéntralo primero; un VACUUM compitiendo contra un horizonte inamovible no logra nada.

Causas comunes

Cómo diagnosticarlo

-- Cuán vieja es la base de datos / las peores tablas:
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;

-- Los tres culpables habituales del horizonte bloqueado:
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;

Cómo solucionarlo

  1. Elimina los bloqueadores del horizonte: termina las transacciones ancianas, haz COMMIT PREPARED / ROLLBACK PREPARED de las transacciones huérfanas de dos fases, elimina los slots de replicación muertos.
  2. Haz vacuum de los peores infractores primero: ejecuta VACUUM (VERBOSE) sobre las tablas con mayor antigüedad de relfrozenxid (o de toda la base de datos si el tiempo lo permite). Con los bloqueadores fuera, las antigüedades bajan y la base de datos vuelve a aceptar escrituras una vez de nuevo bajo el umbral de seguridad.
  3. Después, hazlo estructural: monitoriza age(datfrozenxid) con alertas muy por debajo del techo de 2 mil millones, mantén el autovacuum activo y con recursos adecuados, y alerta sobre transacciones preparadas y slots inactivos — los que sostienen el horizonte en silencio.
🔍 Contexto esencial: VACUUM, autovacuum y bloat — el mecanismo de freezing y cómo el autovacuum se programa a sí mismo.