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
- Una transacción abierta muy vieja fijando el horizonte (una sesión
idle in transactiondurante días o semanas). - Transacciones preparadas huérfanas — creadas por el commit en dos fases, olvidadas para siempre, invisibles a una inspección casual.
- Slots de replicación obsoletos (con feedback) sosteniendo el horizonte en nombre de una réplica que ya no existe.
- Autovacuum deshabilitado o crónicamente falto de recursos en tablas enormes.
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
- Elimina los bloqueadores del horizonte: termina las transacciones ancianas, haz
COMMIT PREPARED/ROLLBACK PREPAREDde las transacciones huérfanas de dos fases, elimina los slots de replicación muertos. - Haz vacuum de los peores infractores primero: ejecuta
VACUUM (VERBOSE)sobre las tablas con mayor antigüedad derelfrozenxid(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. - 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.
