Report #5379
[bug\_fix] Postgres prepared transaction leakage \(xid wraparound or storage bloat\)
Find and rollback/commit lingering prepared transactions using SELECT \* FROM pg\_prepared\_xacts; then ROLLBACK PREPARED 'xid'; or commit them. Prevent by ensuring 2PC is only used with proper transaction managers that clean up after crashes. Root cause: Two-phase commit \(PREPARE TRANSACTION\) reserves XIDs and holds locks until committed/rolled back; if an application crashes after prepare but before commit, the prepared transaction persists indefinitely, blocking vacuum and eventually causing xid wraparound or massive storage bloat.
Journey Context:
Your monitoring alerts that pg\_database.datfrozenxid is approaching 2 billion, nearing the 2.1 billion xid wraparound limit that triggers shutdown. Autovacuum is running constantly but cannot freeze rows. You query pg\_prepared\_xacts and find several entries from weeks ago with transaction IDs holding RowExclusiveLocks on critical tables. You recall a botched deployment where a Java service using Atomikos \(XA\) crashed mid-2PC. The prepared transactions were never resolved, so Postgres keeps those XIDs active, preventing vacuum from removing dead tuples \(since the xmin/xmax must be preserved for the prepared xact\). The database grows by 500GB of bloat because vacuum cannot freeze pages. You immediately run ROLLBACK PREPARED 'gid\_12345'; for each lingering xact. After they are cleared, a manual VACUUM FREEZE reclaims the space and advances datfrozenxid. You then configure max\_prepared\_transactions appropriately and add alerting on pg\_prepared\_xacts age. The fix works because prepared transactions are persistent locks; removing them allows vacuum to proceed and prevents xid exhaustion.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-15T21:10:58.305515+00:00— report_created — created