DEPLOY.PREFLIGHT.FREEZE_HORIZON — A table near its freeze horizon, where an anti-wraparound vacuum will not yield
- Category: safety
- Severity: derived per finding —
highwhen a worker is already running or the table is at or past its threshold,mediumwhile it is merely close - Level: 0
- Downtime class:
blockingon the finding; the rule catalog names none for this check, as it does for every deploy check. The statement this deploy runs will wait, holding its own lock request in the queue, and every reader arriving behind it waits too. - Stability: stable
- Suites: deploy
- Applies to: PostgreSQL. MySQL has no visible freeze horizon — InnoDB's counterpart is purge lag, which is a different measurement against a different failure, and whether that earns a check of its own is its own question.
What it reports
For each table the pending run will take an ACCESS EXCLUSIVE lock on, both freeze clocks and the
cluster thresholds they are held against. A finding appears when the nearer clock is at 90 % of
its threshold or beyond, or when an anti-wraparound autovacuum is already running on that table.
Tables the run does not lock are not examined. This is not a health report about your database.
Why it is a deploy question
An ordinary autovacuum yields: ask for a conflicting lock and it cancels itself, and the ALTER TABLE behind it proceeds within a moment. An autovacuum running to prevent wraparound does not.
It holds its lock until it is finished, because the alternative is a cluster that stops accepting
writes.
On a large table that is hours. And from outside it does not look like a lock at all: pg_locks
shows your statement waiting, the holder is a background worker, and the deploy simply does not
move. The question can this DDL run right now is the one the preflight exists to answer, and the
freeze horizon is one of the few answers an instance can give in advance.
Two clocks, and the second is the one nobody watches
| Clock | Consumed by | Age | Threshold |
|---|---|---|---|
| Transaction ids | writing | age(relfrozenxid) | autovacuum_freeze_max_age |
| Multixacts | locking | mxid_age(relminmxid) | autovacuum_multixact_freeze_max_age |
A multixact is created as soon as a second transaction locks a row somebody else already holds —
the ordinary state of a foreign-key check and of every explicit row lock taken in a SELECT. So a
lock-heavy table can be spotless on the first clock and be the table that stops the cluster.
Both thresholds are read from the server, not hard-coded. autovacuum_freeze_max_age is tunable
and frequently tuned; a check comparing against a fixed 200 million would be wrong in both
directions on such an instance, quietly.
What to do about it
- A worker already running: wait for it, or move the deploy. The finding reports its phase and
block progress from
pg_stat_progress_vacuumso you can tell how long that is. - At or past the threshold, nothing running yet: the launcher will start one, possibly during
your window. Either vacuum the table deliberately at a time you choose —
VACUUM (FREEZE)on a quiet evening advances the horizon without a surprise — or run the deploy now and accept that it may queue. - Merely close: a short deploy will very likely get through. A long one may not.
Never cancel the worker
pg_cancel_backend() on an anti-wraparound worker buys nothing. The launcher starts it again within
autovacuum_naptime, all the work it had done is thrown away, and the horizon is closer than it was
before. The finding says so explicitly, because canceling is the intuitive move and the expensive
one.
A rewrite makes the window longer, not shorter
If the run also rewrites a table — an ALTER COLUMN … TYPE that changes the storage — the finding
says so. The lock is held for the whole rewrite, a vacuum that arrives during it waits, and
everything behind that vacuum waits too.
When it cannot answer
pg_class, pg_stat_progress_vacuum and current_setting() are all readable on a self-managed
instance and are commonly withheld on a managed one. When any of them refuses, the check reports
undetermined with freeze_horizon_unreadable and says what could not be read — never silence,
which would be indistinguishable from a clean answer.
What it does not claim
- That a worker WILL start during your window. It says one can. The launcher's timing depends on
autovacuum_naptimeand on how many workers are free. - That the table carries the cluster's threshold. A per-table
reloptionsoverride is not read, so a table with its ownautovacuum_freeze_max_ageis judged against the wider setting. That over-reports rather than going quiet, and the finding says which numbers it used. - Anything about tables the run does not lock. A table elsewhere in the schema may be far closer to its horizon; that is a monitoring question, and this check is not a monitor.