Transaction ids are 32-bit and wrap. A row whose xmin falls more than 2^31 transactions behind the current xid would appear to be in the future, so Postgres must mark old rows frozen (permanently visible, their xmin no longer compared) before that happens. Freezing is vacuum’s second job, and the quiet one: it happens per page, it dirties the page, and its schedule is controlled by three age settings that only make sense together.
DEMO — xid age over 365 d, toy tablesame chart system as the report
Since Postgres 16 whole pages freeze at once when it is cheap (the WAL is already being written); Postgres 18 adds eager freezing of all-visible pages during normal vacuums.