Robovac
Your table is fine. Your autovacuum settings are the ones Postgres shipped in 2005.
Postgres does not remove old row versions in place. Every delete and every update leaves a dead row behind, and autovacuum comes along later to reclaim them. When it comes along is decided by one formula, a fixed threshold plus a scale factor times the live row count, and that scale factor still defaults to twenty percent. On a table with forty million rows that is eight million dead rows before anything fires at all. Most large tables discover here that the honest answer is every three weeks.
So robovac picks a cadence in time and solves that formula backward, then rounds to a number someone would actually type. The duration comes from Postgres's own token bucket: pages times a weighted page price, over the cost limit, times the delay. The 55/25/20 split of cache hits, misses and dirtied pages is robovac's stated assumption rather than a Postgres constant, and the code says so at the place it is used. The cost limit is chosen off a ladder of six values people really set, and when none of them fits inside the replication lag budget it stops moving that knob and solves for the delay instead.
The interesting part is that it does not trust its own answer. The optimizer runs in four stages, sense, classify, solve, then prove, and prove is a second pass over solve's output that may veto it one setting at a time. If a proposal would lift peak dead rows by more than ten percent, or make the pass slower without buying back any bloat, or push this table's daily vacuum seconds past four times its current load, that setting quietly reverts and its reason string begins "Kept current".
Index bypass is the least obvious thing it computes. Postgres skips the index scan when dead line pointers sit on under two percent of pages, but the threshold counts tuples and the bypass counts pages, and nothing in a statistics snapshot says how they are clustered. So it assumes the worst, one dead tuple per page, and it will veto the cheap outcome on the 32 MB dead-item cap alone even when the page fraction looks fine. Past that point a lower threshold buys more heap passes and leaves the index passes exactly as frequent.
The freeze chain has one number worth explaining. vacuum_freeze_min_age is set to half a vacuum interval's worth of transaction ids, not a whole one, and the reason is a trap. Make the cutoff wider than one interval and a page can go all-visible while every row on it is still younger than the cutoff. Normal vacuums skip all-visible pages, so that page is now visible, unfrozen, and invisible to every later normal pass. Only the aggressive vacuum ever touches it again. Half an interval is the margin that stops it.
- stack
- Next.js · TypeScript · MCP · Redis
- tunes
- 15 settings · 34 explained terms
- access
- none · statistics only, no locks
- stores
- nothing · the url fragment carries the snapshot
- started
- 2026-07-30
- status
- live · actively developed