Wraparound prevention vacuum in Postgres blocking PDMs
Post-deploy migrations failed repeatedly from 03:17 UTC while an anti-wraparound vacuum ran on deployments; deploys blocked about five hours.
PostgreSQL stamps every write with a 32-bit transaction ID and keeps the database writable only while a background job freezes old rows faster than new IDs are spent. This guide reconstructs how that race is run and lost, from the engine's own source and commits, five years of GitLab's public incident tracker, and the issue threads of the tools that collide with it. You leave able to size the job, keep its path clear, and read the gauge before the engine reads it for you.
The problem, stated without the database's vocabulary: a system hands out a finite, reusable sequence number on every write, and can only reuse a number once a background job has certified every row that still carries it. The job is the capacity limit. Nobody provisions it as one.
Why this topic, and why now: in the eight months to October 2026 the PostgreSQL project changed the counter's warning margin, the order in which autovacuum picks tables, the width of two of the engine's other counters, and the text of every warning on the path to the cliff; and on 5 August 2026 an anti-wraparound vacuum blocked GitLab.com's deploys for five hours. The mechanism is twenty years old. The engineering around it is moving fast, and most operators' runbooks have not moved with it.
The arithmetic is short. varsup.c sets the point of real danger at "halfway around from
the oldest potentially-existing XID": the oldest unfrozen transaction ID in the database plus
231, about 2.147 billion transactions. A row is safe to keep past that only if it has been
frozen, marked as visible to everyone so its original ID no longer matters. Freezing is done
by VACUUM. When the oldest unfrozen ID in a table passes autovacuum_freeze_max_age
(default 200 million), autovacuum is forced to run an anti-wraparound vacuum on it whether or
not it is otherwise needed. If that still does not keep up, the engine warns on every ID it hands out
from 100 million before the limit, and from 3 million before the limit it refuses to assign any more:
in the documentation's words, "only read-only transactions can be started"
[1][6].
The failure that recurs in the record is not the database running out of IDs. It is the safety mechanism arriving. PostgreSQL's lock manager cancels an autovacuum that is in someone's way, unless that autovacuum is "working to protect against Xid wraparound" [4]. So the one vacuum you cannot hurry is also the one vacuum that will not step aside, and it arrives on the engine's schedule, not yours. GitLab's deploy pipeline, pg_repack users on Amazon RDS and, by the index of its postmortem, Joyent in 2015 all met it at the lock queue rather than at the cliff. In March 2025 the engine's own developers started moving freezing out of these vacuums for exactly that reason [13].
Who has written this down in public, at what depth: the PostgreSQL project, in source comments and commit messages that argue with each other across a decade; GitLab, whose production and product trackers are public and hold the wraparound incidents, change requests and proposals cited here, from 2019 to August 2026; and the maintainers of pg_repack, pganalyze's collector, Joyent's pgstatsmon and Postgres.ai's how-to library. The three famous wraparound outages (Sentry in 2015, Joyent in 2015, Mailchimp's Mandrill in 2019) sit behind hosts this session could not reach; they appear here only as one-line entries in a public index [42], and nothing is claimed from them beyond those lines.
Scope. Covered: transaction-ID and multixact-ID wraparound in PostgreSQL, the vacuum machinery that prevents it, what blocks that machinery, and how operators measure and schedule it. Not covered: table and index bloat as a performance problem except where it slows freezing, sequence and primary-key exhaustion (see the earlier guide on choosing the next ID), and managed-service internals that are not public.
The common shape across the engine's design and every operator in the corpus: a consumer that spends the counter, a horizon that limits what may be frozen, a throttled job that freezes, and, at the operators who have been bitten, a second layer that measures and schedules the job instead of trusting it.
The consumer. Every transaction that writes takes an XID; read-only transactions do not. That
sounds like a property of the workload, and it is, but the record shows maintenance spending the budget
too. In February 2021 GitLab installed cron jobs that ran ANALYZE by hand to work around
stale planner statistics after a monthly bulk update [31]; two days later it opened a
change to remove them because "Ongres recognized we are near emergency autovacuum to prevent
wraparound". The fix for one symptom was eating the margin of another. The engine's own docs make the
same point about recovery: do not reach for VACUUM FULL at the stop limit, "because it
requires an XID" [6].
The horizon. Freezing can only certify rows older than the oldest snapshot anyone still holds.
The documentation lists the holders in the order to remove them: old prepared transactions, long-running
open transactions, and replication slots, including slots "created for replication to servers that no
longer exist" [6]. GitLab's incidents add two the docs do not dwell on. A migration's
CREATE INDEX ran for five and a half hours before anyone looked [21],
and in June 2022 autovacuum's own ANALYZE, throttled by its cost budget and "not constrained
by statement_timeout", held a snapshot for 39 minutes [34]. GitLab's
repack runbook now states the consequence plainly: "A pinned xmin blocks autovacuum
database-wide, not just on the target table" [27].
The job. Autovacuum is a launcher plus a fixed pool of workers, throttled by a cost model so it
does not compete with production I/O. The pool size, autovacuum_max_workers, needs a restart
to change, as does autovacuum_freeze_max_age itself, which is PGC_POSTMASTER
[5]; the source comment explains that updating shared state from a configuration hook
"doesn't look practical". Until March 2026 workers took tables in catalogue order; Nathan Bossart's
commit adding a score says the list was processed "in the same order as the list", and that under the
new scheme "a table nearing transaction ID wraparound should be vacuumed first" [15].
That change ships in version 19. Every production system in this corpus ran without it.
The operator layer. This is the component that appears only after an incident. GitLab's
arrived in a burst. On 18 May 2021 an on-call engineer estimated the database was 12 hours from
shutdown; it was later found to be 27 days out [21]. The corrective action was a
metric. The merge request that delivered it, merged 24 May, reveals that the metric had existed all
along: it was renamed because "the old metric was not consumed in any dashboards, alerts or recording
rules", and its multixact half was switched to mxid_age "to correct the age calculation"
[22]. Three days later the runbooks gained a severity-1 saturation point measuring
age against 231 minus 3 million [23], and on 2 June a second one that is the
most useful idea in this guide: total autovacuum time per worker over a day, divided by the time left
until the wraparound horizon at the current XID rate [24]. It is a capacity ratio. It
says whether the job could finish a full pass before the counter runs out, which an age chart cannot.
Since 9.6 the visibility map carries an all-frozen bit per page, so an anti-wraparound vacuum skips pages already frozen instead of rescanning the table. Before it, the 2016 commit says, wraparound meant "full-table scans" every time.
Source: a892234, 2016
From version 14, a vacuum that finds its table past 1.6 billion stops throttling and skips index vacuuming to freeze as fast as it can. Its authors expected it to fire "long after the autovacuum began".
Source: 1e55e7d, 2021
GitLab's ratio of vacuum time to time-until-horizon, with a soft SLO at 20% and a hard one at 33%. The only artefact in the corpus that treats freezing as throughput rather than as a state.
Source: GitLab runbooks, 2021
What the engine and its operators chose, what they rejected, and why. Where the reason lives on a host this session could not reach, the guide says so rather than inventing it.
The deepest decision is the width of the counter, and the repository records it as a long series of
partial answers rather than one. A 32-bit XID per row header was the original choice. InnoDB, by
contrast, stores a 6-byte transaction ID per row, DATA_TRX_ID_LEN = 6 [44],
which at a sustained million transactions a second lasts about 8.9 years (248 / 106
seconds; an estimate at an illustrative rate). OrioleDB, a storage engine for PostgreSQL, uses 64-bit
IDs and claims to eliminate "the well-known and painful wraparound problem", while saying it "is not
recommended for production usage" [43]. PostgreSQL itself has widened around the
problem: SLRU page numbers in November 2023, after "repeated bugs in the area of handling SLRU
wraparound in the past, some of which have caused data loss" [17]; multixact member
offsets in December 2025, while "Multixids are still limited to 2^31" [18]; and the
OID counter in September 2026 [19]. At HEAD the row XID is still 32 bits. The
argument for why the full 64-bit patch keeps returning from review is on the commitfest and mailing-list
hosts, which were unreachable; this guide records the outcome and not the reasons.
autovacuum_freeze_max_age at 200M and move freezing into low-load windows first.That fork is a real disagreement between two credible sources, and the difference in context explains it. The PostgreSQL documentation prices the threshold in storage: 200 million transactions "translates to about 50MB of pg_xact storage and about 2GB of pg_commit_ts storage" [6], so on a large database raising it costs almost nothing. GitLab's database engineer priced it in the size of the eventual mandatory vacuum on multi-terabyte, high-churn tables, and the incident record backs that pricing: the expensive event is not the threshold being crossed but the non-yielding vacuum that follows, at an hour nobody picked [26]. The rule: raise the threshold only on tables whose freeze debt you are already paying down some other way.
VACUUM FREEZE 24 hours ahead of its PostgreSQL 12 upgrade.pg_cancel_backend.The engine tried the third option once. In March 2019 a commit made autovacuum skip anti-wraparound jobs that had been marked non-aggressive, since they were "redundant" [10]. A year later it was reverted and back-patched to 12, because on clusters with many relations it "could cause autovacuum to lock down, with autovacuum workers attempting repeatedly those jobs on the same relations for the same database, that just kept being skipped" [11]. A skipped wraparound job does not go away; it comes back at the front of the queue. That is the same lesson GitLab's on-call learned by hand in 2021, written into the engine's history as a revert.
TRUNCATE or DROP a huge unneeded table rather than vacuum it; the docs keep the 3M margin for exactly that.That change was made on master only. "No back-patch," the commit says, partly because people "might even have monitoring scripts that look for" the old message [9]. The cost of that caution is visible three years later. GitLab's wraparound alert runbook, last edited in July 2026, still tells its on-call that at wraparound "the database will automatically shutdown" and that "recovery would require entering single-user mode" [25]. Its first step is the right one (find and terminate very old transactions), and the misdescribed end state may never be reached; but an engineer who believes the database is about to shut down makes different choices at 03:00 than one who knows it will refuse writes and keep serving reads. Operational knowledge about this failure lags the engine by years, because the failure is rare enough that nobody rereads the runbook until it happens.
| Decision | Chosen | Rejected | Because | Flips when | Evidence |
|---|---|---|---|---|---|
| Row XID width | 32 bits plus freezing; widen neighbouring counters piecemeal | 64-bit row XIDs (OrioleDB, out-of-tree patch); 48-bit as InnoDB | Reasoning on unreachable hosts; repository shows only outcomes | Burn rate makes the freeze pass longer than the horizon even when nothing pins it | bd8d9c9, OrioleDB |
| Freeze threshold | 200M, freeze on a schedule (argued, not yet decided) | Ladder to 1B, deferred | Rarer forced vacuums are larger, and they do not yield | Tables mostly all-frozen already | GitLab, 2022 |
| Blocked by the protective vacuum | Wait; pre-freeze before planned work | Terminate it | It restarts within seconds | The vacuum is the blocked party | GitLab #4215 |
| Skip redundant wraparound jobs | Reverted | Skip (2019) | Workers locked down retrying skipped relations | No condition found in the record | dd9ac7d |
| Recovery at the stop limit | Multi-user VACUUM | Single-user mode | Disables the safeguards, takes the system down | A table must be dropped or truncated | 2406c4e |
| What to alert on | Age as share of limit, and vacuum time against the horizon | Age alone, unwatched | An age chart cannot say whether a pass can finish | Small databases where a full pass takes minutes | gitlab-exporters !224 |
Grouped by failure class. Only one incident in the corpus reached the stop limit. Every other one is the race going badly long before the cliff, which is where an architect can still do something about it.
The classes, as I am naming them (the sources use a different word each time): the pin, something holds the horizon so freezing cannot advance; the treadmill, a vacuum runs but never completes a pass; the guard that will not step aside, the mandatory vacuum colliding with a lock someone else needs; the pile-up, many tables reaching the threshold together; and the misread gauge, the number existing but nobody reading it correctly.
deployments can always get its lock within the timeout, or retry and succeed.Deploys-blocked-gprd::5hr and the same for staging. No user-facing outage.pg_cancel_backend on the vacuum.ci_builds index "blocked wraparound-prevention autovacuum on gitlab.com ... for over 18 hours".SLO::Missed.ci_builds made each vacuum fail during index cleanup ("failed to re-find parent key in index"), before the heap was frozen. "We suspect this had been running in a loop since the corruption occurred, and that nothing is logged."CREATE began at 13:30 and was found at 18:33 after dead-tuple alerts; it was halted at 00:36 the next day. On-call estimated 12 hours to shutdown and "a maximum of 15 minutes warning".VACUUM FREEZE of candidate tables a day before the PG12 upgrade (#4215).The pile-up has an engine-side confirmation. The 2020 commit that added insert-triggered autovacuum says that before it, insert-only tables could receive their first vacuum ever as an anti-wraparound vacuum, and that "new clusters, or recently pg_restored clusters" could have many large tables with "the same relfrozenxid, which could result in large numbers of tables requiring an anti-wraparound vacuum all at once" [12]. A restore or a migration resets every table's age to the same value. At GitLab's burn rate, three days of quiet later they all come due together.
The treadmill shows up again in 2026, at scale and without corruption. GitLab's database team, gathering
signals from self-managed customers, reports "multi-day autovacuum runs, including the dreaded
autovacuum: VACUUM <table> (to prevent wraparound) holding a lock for 5+ days", and one
case where a customer's ANALYZE "was blocked for 2 days behind a wraparound autovacuum that
had already been running for 5" [35]. The cause it names is default throttling:
cost delays of 10 to 20 ms, three workers, small maintenance_work_mem. "Customers only
discover the issue after it has caused an incident." Compare GitLab.com's own settings: six workers, a
5 ms delay and a cost limit of 6,000 [26]. The same application, with a cost budget of 1,200 units per millisecond of delay against 10 to 20
on the customers' settings (6,000 / 5 against 200 / 10 or 200 / 20; an estimate of relative
throughput, ignoring worker count): roughly two orders of magnitude apart in how fast its janitor is
allowed to work.
ANALYZE runs are harmless.ANALYZE held its snapshot for 39 minutes; no other vacuum could remove anything newer meanwhile.The public index of postmortems summarises Sentry's 2015 outage as down "for most of a working day", Mandrill's 2019 one as "a partial outage lasting a day and a half", and Joyent's 2015 Manta outage as "PostgreSQL's transaction wraparound maintenance taking a lock on something, and a Joyent query that unnecessarily tried to take a global lock" [42]. The last of those is the guard-at-the-lock-queue class again, nine years before GitLab's. Their full write-ups were unreachable from this session; read them before relying on any detail beyond these lines.
The misread gauge is the class that connects the others. GitLab's metric existed but was "not consumed in any dashboards, alerts or recording rules", and computed the multixact age with the wrong function [22]. pganalyze's collector, a commercial monitoring agent, matched autovacuum log lines in a way that missed "anti-wraparound autovacuum, without aggressive" until a fix merged on 23 September 2026 [40]. Joyent's monitoring agent has an open issue asking that these vacuums be labelled "wraparound" rather than "aggressive" [39], and Postgres.ai's guidance notes multixact wraparound is "usually missing in observability tools" [41]. Even the engine conceded the point in March 2026, adding a percentage to its warnings because "this more clearly expresses the urgency of the situation" [16]. Four independent parties, one lesson: the number is easy to collect and hard to read.
The engine's limits are measured from source at a stated branch. Operator figures are reported by the operator. Derived rows show their arithmetic and are labelled estimates.
| Metric | Value | At | Context | As of | Source |
|---|---|---|---|---|---|
| Usable XID distance before the hard limit | 231 ≈ 2.147B | PostgreSQL | From the oldest unfrozen XID in the cluster | HEAD 2026-10 | varsup.c |
| Stop margin | 1M → 3M | PostgreSQL | 12 and 13 stop at 1M; 14 onward at 3M, because of "edge-case bugs" near the limit | 2020-08 | cd5e822 |
| Warning margin | 11M → 40M → 100M | PostgreSQL | 12: 10M before the stop; 14: 40M; development branch: 100M | 2026-03 | 48f11bf |
| Forced-vacuum threshold | 200M | PostgreSQL | Default; restart needed to change globally; per-table reloption allowed | HEAD 2026-10 | guc_parameters.dat |
| Failsafe threshold | 1.6B | PostgreSQL | Version 14 onward; drops cost delay and index vacuuming | 2021-04 | 1e55e7d |
| Storage cost of the 200M horizon | ~50 MB / ~2 GB | PostgreSQL | pg_xact and pg_commit_ts respectively | HEAD 2026-10 | maintenance.sgml |
| Time to burn 200M XIDs | < 3 days | GitLab.com | Main database, on average | 2022-01 | GitLab runbook |
| Implied burn rate and full headroom | > 67M/day; ~32 days | GitLab.com | Estimate: 200M / 3 days; (231 − 3M) / 67M per day, if freezing stopped entirely | 2022-01 | derived |
| Database age when monitoring was fixed | 199,895,030 | GitLab.com | age(datfrozenxid) on the primary, against a 200M threshold | 2021-05-21 | !224 |
| Wraparound saturation SLOs | 60% / 70% | GitLab.com | Soft and hard, as share of 231 − 3M; severity 1 | 2021-05 | 4ef250e |
| Vacuum-capacity SLOs | 20% / 33% | GitLab.com | Daily vacuum seconds per worker over seconds to horizon | 2021-06 | 9f15781 |
| Autovacuum settings | 6 workers, 5 ms, 6000 | GitLab.com | Workers, cost delay, cost limit | 2022 | GitLab runbook |
| Anti-wraparound vacuum duration on defaults | 5+ days | Self-managed GitLab | Customer signals; one ANALYZE blocked 2 days | 2026-05 | #600630 |
| Reindex blocking the guard | > 18 h | GitLab.com | ci_builds, weekend automatic reindex | 2022-10 | #377720 |
| Deploys blocked by the guard | 5 h | GitLab.com | Production and staging deploy label | 2026-08 | #22642 |
| Reads and writes blocked | > 30 min | RDS user | pg_repack temp table under anti-wraparound vacuum | 2023-11 | pg_repack #371 |
| Practitioner danger line | > 1B | Postgres.ai | "Values above 1B have to be considered dangerous" | undated | howto 0044 |
| InnoDB per-row transaction ID | 48 bits; ~8.9 years at 1M tx/s | MySQL 8.4 | Estimate for the duration: 248 / 106 s, illustrative rate | 8.4 branch | data0type.h |
What to plan against. Two quantities decide whether you are safe, and the engine reports
neither directly. The first is your burn rate: XIDs per day, read from txid_current()
deltas on the primary. Divide the 2.14 billion usable IDs by it and you have your total runway from a
fully frozen state; GitLab.com's was about a month. The second is your full-pass time: how long a
complete freeze of your largest, least-frozen tables takes at your actual cost settings. GitLab's
ratio of the two, with a hard SLO at one third, is the closest thing in the public record to a sizing
rule, and it is a sensible one. A pass that takes a third of your runway leaves two thirds for
everything in the failure section to go wrong. Nobody has published a measured full-pass time for a
named multi-terabyte table, which is the number you most need and will have to measure yourself.
The version-specific limits matter: a runbook written against 12 or 13 has a 1M stop margin and a warning only 10M before it, and on those versions the single-user advice was still in the server's own hint text. The 100M warning and the table prioritisation are on the development branch for version 19; production clusters in October 2026 run without them. GitLab's three-day figure is from January 2022 and its workload has grown since; treat it as an order of magnitude for a large SaaS, not a benchmark.
Graded by what each artefact can prove. The corpus is three hosts deep (GitHub, GitHub raw content, GitLab) because those were the only hosts this session could reach; there are no papers or talks, and the famous blog postmortems are cited only through an index.
Post-deploy migrations failed repeatedly from 03:17 UTC while an anti-wraparound vacuum ran on deployments; deploys blocked about five hours.
A self-managed database went read-only after a corrupt index made every vacuum of ci_builds abort before freezing, silently.
An 11-hour migration held the horizon; on-call estimated 12 hours to shutdown, later corrected to 27 days. Monitoring followed within a week.
A weekend reindex held a lock that blocked the wraparound vacuum on ci_builds for over 18 hours.
The primary shut down at over 90% load with several anti-wraparound vacuums and a reindex in flight.
Manual ANALYZE crons installed as an incident workaround were removed because the database was "near emergency autovacuum to prevent wraparound".
Autovacuum's own throttled ANALYZE held a snapshot for 39 minutes, preventing cleanup everywhere.
On RDS PostgreSQL 14, an anti-wraparound vacuum on a repack temp table and repack's drop step together blocked a table for over half an hour.
The same interaction three years earlier: "The combination of the two made the database inaccessible." Unanswered and open.
Where the hard, warning and stop limits are computed, with the comment refusing to make the warning configurable.
A blocked backend cancels an autovacuum only if it is not flagged PROC_VACUUM_FOR_WRAPAROUND.
Past 1.6 billion, vacuum drops its cost delay and skips index work; designed to fire late inside an already-running wraparound vacuum.
A skip optimisation locked autovacuum into retrying the same relations; reverted and back-patched to 12.
Moves freezing into insert-triggered vacuums because "anti-wraparound vacuums are not automatically canceled by conflicting lock requests".
Insert-only tables used to get their first vacuum as a wraparound vacuum, many at once after a restore.
Before version 19, tables near wraparound waited their turn in catalogue order.
Wraparound bugs "have caused data loss"; the widening proceeds one counter at a time, and row XIDs stay 32-bit.
The stop margin grew because of "edge-case bugs" near the limit; the warning grew for workloads that burn IDs fast.
The metric existed, unwatched, with the multixact age computed by the wrong function; database age was 199.9M at the time.
Vacuum seconds per worker per day divided by seconds until the wraparound horizon; soft 20%, hard 33%.
Wraparound as a severity-1 saturation point against 231 minus 3M, SLOs at 60% and 70%.
A monitoring agent's log parser missed one class of anti-wraparound vacuum until September 2026.
Manta's operator asks its monitor to name wraparound vacuums distinctly; the change log records splitting them out.
DATA_TRX_ID_LEN = 6: InnoDB pays two more bytes per row to push the counter's horizon out by a factor of 65,536.
Withdraws single-user-mode advice as "terrible", corrects "shut down" to "refuses commands that assign" IDs; master only.
200M reached in under three days; a ladder to 1B proposed and argued down in favour of scheduled freezing.
Right first step (terminate old transactions); still predicts shutdown and single-user recovery three years after upstream withdrew both.
Repacks are gated on freeze age because the forced vacuum "cannot be deferred or skipped" and a pinned xmin would block it.
Proposal to ship freeze-age monitoring in the product's admin pages, MultiXact included, with escalation above about 1.5 billion.
A six-hour scheduled freeze 24 hours before the PG12 upgrade, because a terminated wraparound vacuum "will restart in seconds".
Tables over 95% of the threshold given a 500M per-table limit to keep forced vacuums out of the upgrade, then reset.
Default throttling yields wraparound vacuums lasting 5+ days; the team's tuning playbook is not in customer docs.
Thresholds from practice: alert from roughly 200M to 1B, treat above 1B as dangerous; multixact often unmonitored.
One-line summaries of the three best-known wraparound outages; their source pages were unreachable here.
Read-only at the stop limit; recovery order; why not single-user mode, VACUUM FULL or VACUUM FREEZE.
64-bit transaction IDs and no dedicated VACUUM, claimed; "not recommended for production usage".
Six rungs on a disposable PostgreSQL container. The first three make the mechanism visible; the last three are the operator layer GitLab built after its incidents.
Start PostgreSQL with autovacuum_freeze_max_age at its minimum, run pgbench, and chart
age(datfrozenxid) and per-table age(relfrozenxid) every ten seconds.
Done when: pg_stat_activity shows a worker whose query ends in "(to prevent wraparound)" and the age drops after it. Teaches: the sawtooth, and that read-only queries do not move it.
In a second session open a REPEATABLE READ transaction and leave it idle. Keep pgbench running.
Done when: the wraparound vacuum runs to completion and the age does not fall. Teaches: effort behind a pinned xmin freezes nothing; remove the pin before adding workers.
Load a table large enough that its vacuum takes minutes. Run ALTER TABLE ... ADD COLUMN with lock_timeout against an ordinary autovacuum, then against an anti-wraparound one.
Done when: the log shows "canceling autovacuum task" in the first case and your ALTER timing out in the second. Teaches: the proc.c rule, and why deploy pipelines need a freeze-age gate.
Build the xid_wraparound test module from the PostgreSQL tree (its 2023 commit calls the helpers "handy for manually testing XID wraparound") and use it to consume IDs to the warning and stop limits on a throwaway cluster.
Done when: you recover from the stop limit in multi-user mode, in the docs' order, without single-user mode. Teaches: what the error actually blocks, and that reads keep working.
Export age as a share of 231 − 3M (XID and multixact), and GitLab's ratio of daily vacuum seconds per worker to seconds until the horizon at the measured burn rate. Alert on the ratio, and on age that has not fallen for a day.
Done when: an injected pin from rung 2 fires the "not falling" alert well before the age alert. Teaches: capacity, not state, is the signal.
Write a job that freezes the largest tables over about 80% of the threshold in a low-load window, oldest first, and a pre-check that refuses DDL, repack and reindex jobs on tables within range.
Done when: a week of production-like load produces no anti-wraparound vacuum on any table above your size cut-off. Teaches: the forced vacuum is a missed appointment; schedule it and it never comes.
The queries that produced this corpus. GitLab's API serves public issue descriptions without authentication, and the PostgreSQL history is searchable from a blobless clone.
curl "https://gitlab.com/api/v4/projects/gitlab-com%2Fgl-infra%2Fproduction/issues?search=wraparound&per_page=60"curl "https://gitlab.com/api/v4/projects/gitlab-org%2Fgitlab/issues?search=freeze_max_age""to prevent wraparound" site:github.com issuesgit log --date=short -i --grep=wraparound --format="%h %ad %an %s"git log --date=short -i --grep=failsafe --format="%h %ad %s"git log -S"xidWrapLimit - 3000000" -- src/backend/access/transam/varsup.cgit clone --depth 1 https://gitlab.com/gitlab-com/runbooks.git && grep -ril wraparound runbooks/docs"mxid_age" "datfrozenxid" exporter"wraparound" pg_repack OR pg_partman OR reindex is:issue"PROC_VACUUM_FOR_WRAPAROUND" lock cancelNumbering follows the evidence ledger in sources.md; ledger rows that support no claim on this page are omitted. All checked 2026-10-16.