Skip to content
Dev Tools Article

Postgres's 2-Billion-Transaction Time Bomb Is Still Ticking

Wraparound took down Sentry and Mandrill, yet 64-bit XIDs remain stuck outside core Postgres.

Mariana Souza
Mariana Souza
Senior Editor · Aug 20, 2026 · 4 min read
Postgres's 2-Billion-Transaction Time Bomb Is Still Ticking

Every few years, transaction ID wraparound resurfaces on the front page of Hacker News, usually attached to a postmortem. Sentry lost most of a US working day to it in July 2015. Mailchimp's Mandrill went down in February 2019 when one hot shard blew through its XID budget — engineers had flagged the risk the previous November and left a monitoring ticket in the backlog. A fresh explainer making the rounds this week is a reminder that a new cohort of developers inherits this failure mode every year, and that some of the folklore around it is wrong.

Here's the short version, the corrections, and the uncomfortable question underneath: why does the world's most-loved relational database still have a 2-billion-transaction time bomb in 2026?

A 1990s space optimization with a long tail

PostgreSQL's MVCC stamps every row with the 32-bit transaction ID that created it. Visibility checks compare XIDs using modulo-2³² arithmetic, which means each transaction sees roughly 2 billion XIDs as "the past" and 2 billion as "the future." The design bought 4 bytes per row-header field back when RAM was measured in megabytes, and it works fine — as long as VACUUM periodically freezes old rows, marking them visible to everyone so their XIDs can be recycled.

If freezing falls behind by ~2 billion transactions, a row's creation XID would flip from "distant past" to "future" and the row would vanish from query results. That's the horror-story version, and it's what most blog posts (including this week's) lead with. In practice, modern Postgres never lets you get there, and the actual failure mode is different: downtime, not data loss.

The real sequence, per the current docs: at 40 million transactions from the limit, the log fills with WARNING: database "mydb" must be vacuumed within N transactions. At 3 million remaining, Postgres refuses to assign new XIDs — writes fail, reads keep working. The explainer circulating now cites an 11-million cutoff; older material cites 1 million (that was the threshold in Sentry's era). If you're writing runbooks, use the docs for your major version, not a blog post. One more piece of stale folklore worth killing: you no longer need to shut down and vacuum in single-user mode. The docs now explicitly say that's "not necessary or desirable" — find the blocker, then run a plain VACUUM.

Autovacuum isn't the problem. What blocks it is.

By default, autovacuum forces an aggressive anti-wraparound vacuum once a table's relfrozenxid age passes autovacuum_freeze_max_age (200 million transactions). So on paper, you'd need to outrun ten forced-vacuum cycles to hit the wall. Nobody loses this race because autovacuum is lazy; they lose it because something silently pins the freeze horizon:

  • Long-running transactions — an analyst's forgotten BEGIN, a stuck migration — hold back the oldest XID vacuum can freeze.
  • Abandoned replication slots retain the horizon indefinitely. A decommissioned replica that nobody dropped the slot for is a classic.
  • Orphaned prepared transactions from a two-phase-commit coordinator that crashed mid-flight. Check pg_prepared_xacts; most teams don't know it exists until it hurts.
  • Sheer write volume on huge tables, where the anti-wraparound vacuum itself takes days — Mandrill's case. The vacuum was running; it just couldn't finish in time.

The monitoring story fits in one query, which is exactly why it's embarrassing how many teams don't run it:

SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database
ORDER BY xid_age DESC;

Alert at 500 million, page at 1 billion. RDS and Aurora expose this as MaximumUsedTransactionIDs; if you're on managed Postgres and don't have that alarm, set it today. Pair it with checks on pg_stat_activity for old backend_xmin, pg_replication_slots for inactive slots, and pg_prepared_xacts.

Postgres chose guardrails over a fix

Since Mandrill, the community's response has been steady hardening rather than root-cause surgery. Postgres 9.6's freeze map stopped aggressive vacuums from re-reading already-frozen pages, turning multi-day vacuums on append-mostly tables into fast ones. Postgres 14 added the failsafe: once a table's age passes vacuum_failsafe_age (default 1.6 billion), vacuum drops its cost-based throttling, skips index cleanup, and sprints to freeze the table before the write-stop threshold. Between the failsafe and one CloudWatch-style alarm, a 2026 deployment has to ignore weeks of escalating signals to actually hit the wall.

But the wall is still there, and the honest fix — 64-bit XIDs — has been sitting in the commitfest queue for years. It's not that nobody cares; it's that changing the on-disk heap page format without breaking pg_upgrade for billions of existing pages is genuinely hard, and the patch series keeps getting reworked. Meanwhile OrioleDB, the cloud-native storage engine being built on Postgres's table AM interface, ships 64-bit XIDs today and markets wraparound elimination as a headline feature. Postgres Pro's commercial fork has offered the same for years. The capability exists; core Postgres just can't swallow the migration cost yet.

What to actually do

If you run Postgres in production, this week's assignment is thirty minutes: add the age(datfrozenxid) alert, sweep for dead replication slots and prepared transactions, and confirm you're on 14 or newer so the failsafe has your back. If you're designing new write-heavy systems — event ingestion, queues-on-Postgres, anything appending billions of rows — treat XID budget as a capacity dimension alongside disk and IOPS, and consider partitioning specifically so anti-wraparound vacuums work on bounded chunks.

And calibrate your fear correctly. Wraparound stopped being a data-loss story a long time ago; it's an availability story with days of warning attached. The teams it still takes down aren't unlucky — they're unmonitored. That's the least glamorous kind of outage there is, and the easiest to never have.

Sources & further reading

  1. WTH is PostgreSQL Transaction ID Wraparound? — dev.to
  2. Routine Vacuuming — PostgreSQL Documentation — postgresql.org
  3. Transaction ID Wraparound in Postgres — blog.sentry.io
  4. What We Learned from the Recent Mandrill Outage — mailchimp.com
  5. 64-bit XIDs — PostgreSQL Commitfest — commitfest.postgresql.org
  6. OrioleDB Documentation — orioledb.com
Mariana Souza
Written by
Mariana Souza · Senior Editor

Mariana covers the fast-moving world of machine learning and generative AI, with a particular focus on how these technologies are reshaping development workflows. When she isn't stress-testing the latest foundation models, she's usually at a local hackathon.

Discussion 1

Join the discussion

Sign in or create an account to comment and vote.

Emma Lindgren @excited_emma · 23 minutes ago

wild that we're still dealing with this in 2024. monitoring helps but doesn't solve the underlying problem—postgres needs 64-bit XIDs in core, full stop.

Related Reading