Data Platforms 16 Oct 2026 35 min read

When maintenance loses the race: the vacuum PostgreSQL will not let you skip

How production PostgreSQL operators keep the 32-bit transaction-ID counter from running out: the anti-wraparound vacuum that freezes old rows, what pins or stalls it, why it will not yield to lock waiters, and how operators measure and schedule it, reconstructed from the engine's source and commits, GitLab's public incident tracker and runbooks, and tool maintainers' issue threads.

PostgreSQL stays writable only while a throttled background job freezes old rows faster than write traffic spends a 2^31 transaction budget. This guide reconstructs how that race is run and lost from the engine's own code and commit history, five years of GitLab incidents and runbooks, and the pg_repack, pganalyze and Joyent records. A reader leaves able to size freezing as throughput against a deadline, keep the xmin horizon clear, gate deploys and repacks on freeze age, and recover at the limit without the single-user mode most runbooks still prescribe.

The finding that surprised me

The recurring failure is not the database running out of IDs but the safety vacuum arriving: proc.c cancels a blocking autovacuum only if it is not working to prevent wraparound, so the one vacuum you cannot hurry is the one that never steps aside, and GitLab, pg_repack users on RDS and (per the postmortem index) Joyent all met it at the lock queue rather than at the cliff.

What you get out of it

  • The anti-wraparound vacuum is the only autovacuum the lock manager will not cancel; in March 2025 the engine began moving freezing into insert-triggered vacuums for that stated reason, and GitLab now gates repacks on freeze age because it 'cannot be deferred or skipped'.
  • Freezing is a capacity, not a chore: GitLab.com spent the 200M forced-vacuum budget in under three days (more than 67M XIDs a day, about a month of total runway), and its runbooks measure daily vacuum time per worker against time to the horizon with a hard SLO at one third.
  • The number is easy to collect and hard to read: GitLab's wraparound metric existed unwatched, with the multixact age computed by the wrong function, until a May 2021 near miss whose on-call estimate of 12 hours to shutdown was really 27 days.
  • A vacuum that runs is not a vacuum that freezes: a corrupt index made every vacuum of one customer's ci_builds abort before freezing until the database went read-only, and a 2019 engine optimisation that skipped wraparound jobs locked autovacuum into retrying them and was reverted.
  • Operator runbooks lag the engine by years: upstream called single-user recovery 'terrible advice' in October 2023 (master only, no back-patch), yet GitLab's alert runbook, edited July 2026, still predicts shutdown and single-user recovery.

Scope

Why this, now. Between March and October 2026 PostgreSQL raised its wraparound warning margin to 100M, added wraparound-first autovacuum prioritisation, widened its OID counter to 8 bytes and reworded its warnings, while on 5 August 2026 an anti-wraparound vacuum blocked GitLab.com's deploys for five hours.

What it does not cover. Table and index bloat as a performance topic, sequence and primary-key exhaustion, managed-service internals, and the content of the Sentry, Joyent and Mandrill postmortems and of the PostgreSQL mailing lists and commitfest, which were unreachable from this session.

Open the field guide → Self-contained: it loads nothing at read time, follows your system theme, and prints cleanly.