Vacuum meters itself with a token budget: page touches cost units, and when the running balance reaches autovacuum_vacuum_cost_limit, the worker sleeps autovacuum_vacuum_cost_delay milliseconds. The default fell from 20 ms to 2 ms in Postgres 12 (a 10× speedup), but clusters initialized earlier often still carry 20 ms in their config, throttling every vacuum to a crawl on hardware that stopped caring a decade ago.
DEMO — one vacuum pass, toy table14 GB heap · 1,740,000 pages
defaults (200 · 2 ms)1 min · 154.7 MB/s
sliders15 min · 15.5 MB/s
autovacuum_vacuum_cost_limit200
cost units the worker may spend before it sleeps · default 200
autovacuum_vacuum_cost_delay20 ms
how long it sleeps · default 2 ms since Postgres 12, 20 ms before
0 disables throttling entirely. Manual VACUUM uses vacuum_cost_delay, which defaults to 0, that is why a hand-run vacuum feels so much faster than autovacuum.