Skip to main content
RunBook Academy

PostgreSQLVIII · VACUUM, Autovacuum and WraparoundVacuum

Autovacuum: launcher, workers and thresholds

Intermediate⏱ ~30 min🧪 Lab requiredpsql

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

Not yet marked complete on this device.

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.

Read-only / Safethe threshold for real tables, computed on the server
$ 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:

EventPoints
Page found in shared buffers1
Page read from disk2
Page dirtied20

When the accumulated cost reaches vacuum_cost_limit (200), the worker sleeps for autovacuum_vacuum_cost_delay (2 ms in PostgreSQL 18) and resets.

Read-only / Safewhat the defaults imply in MB per second
$ 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_slots to specify the maximum number of background workers … With this variable set, autovacuum_max_workers can be adjusted at runtime up to this maximum without a server restart.

Read-only / Safethe two settings and their contexts on 18.6
$ 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 by autovacuum_vacuum_max_threshold in 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_workers is adjustable without a restart in PostgreSQL 18, bounded by autovacuum_worker_slots.
  • Set log_autovacuum_min_duration before 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_autovacuum and dead-tuple counts per table, which is what makes a worker shortage visible before it becomes bloat.

Quiz

Knowledge check · 6 questions

  1. 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?

  2. 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?

  3. 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?

  4. Q4. Which statements about the autovacuum cost model are correct? Select all that apply.

  5. 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.

  6. 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.