PostgreSQLVIII · VACUUM, Autovacuum and WraparoundVacuum
Autovacuum: launcher, workers and thresholds
What you'll learn
- Compute the exact threshold at which a given table becomes eligible
- Convert the cost delay settings into a throughput figure in MB per second
- Explain the relationship between worker slots, max workers and the shared cost budget
- Identify the PostgreSQL 18 changes to autovacuum and what they enable
Prerequisites
Practice
Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27
Autovacuum is two things: a launcher that wakes up, decides which tables need attention and starts workers; and the workers, each of which vacuums one table at a time under a shared throughput budget.
Both halves have arithmetic you can compute, and doing so converts most autovacuum questions from opinion into measurement.
The launcher
The launcher aims to start a worker in each database once per
autovacuum_naptime — 60 seconds by default. It spreads that work
across the interval rather than doing it all at once, so with N
databases it starts a worker roughly every autovacuum_naptime / N
seconds. The worker then builds its own list of tables in that database
that need work and processes them.
Two consequences of the design that trip people up:
The cadence per database is naptime, but the launcher is busier the
more databases you have. With ten databases and a 60 second naptime,
each database is still visited about once a minute — via a new worker
started roughly every six seconds. What that costs is worker slots: ten
databases needing attention at once will exhaust
autovacuum_max_workers long before naptime is the limiting factor.
A worker processes its list serially. One worker on a database with
three enormous tables that all need vacuuming does them one after
another. This is why autovacuum_max_workers and table sizes interact.
The eligibility arithmetic
A table becomes eligible for vacuum when:
n_dead_tup > autovacuum_vacuum_threshold
+ autovacuum_vacuum_scale_factor × n_live_tup
capped, in PostgreSQL 18, at autovacuum_vacuum_max_threshold.
On defaults that is 50 + 0.2 × n_live_tup, capped at 100,000,000.
$ psql -U postgres -c "SELECT relname, n_live_tup, n_dead_tup, (current_setting('autovacuum_vacuum_threshold')::numeric + current_setting('autovacuum_vacuum_scale_factor')::numeric * n_live_tup) AS scale_based, least(current_setting('autovacuum_vacuum_threshold')::numeric + current_setting('autovacuum_vacuum_scale_factor')::numeric * n_live_tup, current_setting('autovacuum_vacuum_max_threshold')::numeric) AS effective_threshold FROM pg_stat_user_tables WHERE n_live_tup > 0 ORDER BY n_live_tup DESC LIMIT 6" relname | n_live_tup | n_dead_tup | scale_based | effective_threshold
------------------+------------+------------+-------------+---------------------
pgbench_accounts | 500000 | 20428 | 100050.0 | 100050.0
pgbench_history | 481174 | 0 | 96284.8 | 96284.8
vac_scatter | 100000 | 0 | 20050.0 | 20050.0
vm_demo | 50000 | 0 | 10050.0 | 10050.0
churn | 50000 | 8000 | 10050.0 | 10050.0
fsm_control | 50000 | 50000 | 10050.0 | 10050.0
(6 rows)fsm_control has 50,000 dead against a threshold of 10,050 and is
overdue. It has autovacuum_enabled = off set on it from an earlier
experiment, which is why nothing has collected it — and it is a good
illustration of how invisible that setting is once made.
There are two other triggers with the same shape:
Analyze: autovacuum_analyze_threshold (50) plus
autovacuum_analyze_scale_factor (0.1) times live tuples, counting
inserts, updates and deletes.
Insert-only vacuum: autovacuum_vacuum_insert_threshold (1000) plus
autovacuum_vacuum_insert_scale_factor (0.2) times live tuples. This
exists because an append-only table produces no dead tuples and would
otherwise never be vacuumed, never build a visibility map, and never
freeze — accumulating a single catastrophic aggressive vacuum for later.
The throughput ceiling
This is the arithmetic that decides whether autovacuum can keep up, and almost nobody computes it.
Vacuum accumulates cost points as it works:
| Event | Points |
|---|---|
| Page found in shared buffers | 1 |
| Page read from disk | 2 |
| Page dirtied | 20 |
When the accumulated cost reaches vacuum_cost_limit (200), the worker
sleeps for autovacuum_vacuum_cost_delay (2 ms in PostgreSQL 18) and
resets.
$ psql -U postgres -f cost-model.sql cost_limit | delay_ms | mb_per_s_all_dirtied | mb_per_s_all_missed | mb_per_s_all_cached
------------+----------+----------------------+---------------------+---------------------
200 | 2 | 39 | 391 | 781
(1 row)Roughly 39 MB/s in the worst case where every page is dirtied.
Workers and slots in PostgreSQL 18
Before 18, autovacuum_max_workers required a restart to change,
because shared memory for worker slots was sized from it at startup.
That has changed. Verified against the release notes:
Add server variable
autovacuum_worker_slotsto specify the maximum number of background workers … With this variable set,autovacuum_max_workerscan be adjusted at runtime up to this maximum without a server restart.
$ psql -U postgres -c "SELECT name, setting, boot_val, context FROM pg_settings WHERE name IN ('autovacuum_worker_slots','autovacuum_max_workers')" name | setting | boot_val | context
-------------------------+---------+----------+------------
autovacuum_max_workers | 3 | 3 | sighup
autovacuum_worker_slots | 16 | 16 | postmaster
(2 rows)autovacuum_worker_slots reserves the shared memory at startup;
autovacuum_max_workers decides how many of those slots may be in use,
and can now be raised during an incident without a restart. Setting
autovacuum_max_workers above autovacuum_worker_slots has no effect.
What to take from this
- Eligibility is
threshold + scale_factor × live_tuples, capped byautovacuum_vacuum_max_thresholdin PostgreSQL 18. - The scale factor means big tables are vacuumed least often, which is backwards; per-table settings are the fix.
- The cost budget is roughly 39 MB/s of dirtied pages on defaults, and it is shared across all workers.
- More workers does not mean more throughput. Raise the cost limit or lower the delay for that.
autovacuum_max_workersis adjustable without a restart in PostgreSQL 18, bounded byautovacuum_worker_slots.- Set
log_autovacuum_min_durationbefore you need it.
Cross-course references
- Linux for Production Sysadmins — Part VI (Processes) covers seeing autovacuum workers as operating-system processes, which is how you confirm the launcher is actually starting them.
- Observability for Production Sysadmins — Part LIX (Database
observability) covers exporting
last_autovacuumand dead-tuple counts per table, which is what makes a worker shortage visible before it becomes bloat.
Quiz
Knowledge check · 6 questions
Q1. Autovacuum is falling behind on one very large, very busy table. A colleague proposes raising autovacuum_max_workers from 3 to 12. What will that achieve?
Q2. An append-only events table has never been updated or deleted, and until PostgreSQL 13 such a table would never have been autovacuumed. What makes it get vacuumed now, and why does that matter?
Q3. A partitioned table has partitions attached and detached every hour by a maintenance job. last_autovacuum on the busiest partition is null despite heavy churn. What is the most likely cause?
Q4. Which statements about the autovacuum cost model are correct? Select all that apply.
Q5. In PostgreSQL 18, autovacuum_max_workers can be raised during an incident without restarting the server, as long as the new value does not exceed autovacuum_worker_slots.
Q6. Autovacuum on a 2 TB table is running continuously and never finishing. Show the arithmetic you would do before changing any setting.
Passing score: 75%. Answers are checked in this browser.