At Mafiree, we help organizations manage and optimize mission-critical PostgreSQL environments, where consistent performance and database health are essential for business operations. One of the most important maintenance mechanisms we monitor and tune is Autovacuum. Although it often runs quietly in the background, Autovacuum plays a critical role in preventing table bloat, maintaining query performance, and protecting databases from transaction ID wraparound.
PostgreSQL is known for its reliability, scalability, and powerful concurrency model, and Autovacuum is one of the key features that keeps it running efficiently. Many DBAs know it exists; fewer understand exactly how it decides what to touch and when — and that gap is often the difference between a high-performing database and one plagued by slow queries, bloated tables, and wraparound failures.
What is Autovacuum?
Autovacuum is PostgreSQL's background maintenance process responsible for:
- Removing dead tuples
- Preventing table and index bloat
- Updating optimizer statistics
- Freezing old transaction IDs
- Preventing transaction ID wraparound
Without Autovacuum, PostgreSQL databases would gradually consume more storage, queries would become slower, and eventually the database could stop accepting writes due to transaction ID exhaustion.
How the autovacuum daemon actually runs
Autovacuum isn't one process — it's a small fleet. A launcher process wakes
up periodically (governed by autovacuum_naptime, default 1 minute), checks every
database, and spins up worker processes to handle whichever tables have crossed
their dirty-tuple threshold. By default, Postgres allows up to
autovacuum_max_workers (3) workers running concurrently across the whole instance.
Each worker does one of two jobs on a table:
Scans the heap, marks dead tuple space as reusable, updates the visibility map, and freezes old row versions so their transaction IDs don't become a wraparound risk.
Resamples the table and refreshes the statistics the query planner relies on for choosing index scans vs. sequential scans, join order, and row estimates.
Why it never shrinks tables on disk
VACUUM — the kind autovacuum runs — never shrinks a table file
on disk. It just marks space as reusable for future inserts and updates. Only
VACUUM FULL physically rewrites the table and returns space to the filesystem, and
it takes an exclusive lock while doing it — not something autovacuum will
ever do for you automatically.
When does a table actually qualify?
Postgres tracks dead tuple counts per table in pg_stat_user_tables. A table becomes
eligible for autovacuum once its dead tuple count crosses this threshold:
vacuum threshold = autovacuum_vacuum_threshold +
(autovacuum_vacuum_scale_factor × n_live_tup)
-- defaults: threshold = 50, scale_factor = 0.2 (20% of the table)
The ANALYZE trigger works the same way, with its own threshold and scale factor
(autovacuum_analyze_threshold = 50, autovacuum_analyze_scale_factor = 0.1).
Vacuum eligibility calculator
Drag the sliders to see when your own table would qualify.
The parameters worth knowing by name
| Parameter | Default | What it controls |
|---|---|---|
| autovacuum | on | Enables or disables the Autovacuum daemon globally. |
| autovacuum_naptime | 1min | How often the launcher wakes up to check for eligible tables. |
| autovacuum_max_workers | 3 | Max concurrent vacuum workers instance-wide. Raising this helps only if I/O and cost limits allow it. |
| autovacuum_vacuum_scale_factor | 0.2 | Fraction of table size (in dead tuples) before a vacuum triggers. Lower this per-table for large tables. |
| autovacuum_vacuum_threshold | 50 | Flat dead-tuple count added to the scale-factor calculation. |
| autovacuum_vacuum_cost_limit | 200 | I/O "budget" a worker spends before pausing. Higher = more aggressive, more I/O pressure. |
| autovacuum_vacuum_cost_delay | 2ms | Sleep time once the cost limit is hit. Lower = faster vacuuming, more I/O contention. |
| autovacuum_freeze_max_age | 200,000,000 | Transaction age at which Postgres forces an aggressive freeze vacuum to avoid wraparound. |
| autovacuum_vacuum_insert_scale_factor | 0 (PG13+) | Triggers vacuum based on inserts alone — matters for insert-only tables that never update or delete. |
9 of 9 parameters shown
VACUUM vs. autovacuum vs. VACUUM FULL
All three share the word "vacuum," and DBAs new to Postgres often assume they're interchangeable — they're not. They differ in who triggers them, what lock they take, and whether they actually shrink the file on disk.
| VACUUM | Autovacuum | VACUUM FULL | |
|---|---|---|---|
| Trigger | Run manually by a DBA / script | Launched automatically by the background daemon based on dead-tuple thresholds | Run manually, deliberately — usually in a maintenance window |
| Lock taken | None — SHARE UPDATE EXCLUSIVE, reads/writes continue | Same as manual VACUUM — non-blocking to normal traffic | ACCESS EXCLUSIVE — blocks all reads and writes on the table |
| Reclaims disk space? | Marks space reusable internally; file size on disk stays the same | Same — internal reuse only, no file shrink | Yes — physically rewrites the table into a new file, returns space to the OS |
| Updates planner stats? | No, unless run as VACUUM ANALYZE | Yes — paired with an ANALYZE cycle based on its own thresholds | No, run ANALYZE separately afterward |
| Typical use case | One-off cleanup, or scripted after a large bulk delete | Routine, ongoing table hygiene — the default and recommended path | Severely bloated tables that need disk space back immediately |
| Cost | Low — throttled, background-friendly | Low — same throttling, self-scheduled | High — full table rewrite + index rebuild, downtime-like impact |
Common VACUUM commands
1. Basic VACUUM
Removes dead tuples and makes space available for reuse.
blogs=# VACUUM;
2. Vacuum a specific table
blogs=# VACUUM employees;
3. VACUUM with ANALYZE
Removes dead tuples and updates planner statistics.
blogs=# VACUUM ANALYZE employees;
4. Analyze only
Updates optimizer statistics without removing dead tuples.
blogs=# ANALYZE employees;
5. VACUUM FULL
Rewrites the table and returns unused disk space to the operating system. Requires an exclusive lock on the table.
blogs=# VACUUM FULL employees;
6. VACUUM FREEZE
Freezes old tuples to prevent transaction ID wraparound.
blogs=# VACUUM FREEZE employees;
7. VACUUM VERBOSE
Displays detailed information about the vacuum process.
blogs=# VACUUM VERBOSE employees;
INFO: vacuuming "public.employees"
INFO: finished vacuuming "public.employees"
8. VACUUM FULL ANALYZE
Reclaims disk space and updates statistics in one pass.
blogs=# VACUUM (FULL, ANALYZE) employees;
Table-level autovacuum settings
Some tables need more aggressive vacuuming than the rest of the cluster. Rather than loosening
these globally in postgresql.conf — which affects every table, including
small, quiet ones — override them on the specific tables that need it:
blogs=# ALTER TABLE code.transactions SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_vacuum_threshold = 5000,
autovacuum_vacuum_cost_delay = 0
);
Should you ever disable Autovacuum?
For production OLTP tables, disabling Autovacuum is generally not recommended, as it can quickly lead to table bloat, outdated statistics, and transaction ID wraparound risks.
blogs=# ALTER TABLE staging.bulk_import SET (autovacuum_enabled = false);
-- Revert to instance-wide defaults for any parameter:
blogs=# ALTER TABLE staging.bulk_import RESET (autovacuum_enabled);
Catching it falling behind and watching it work
blogs=# SELECT schemaname, relname, n_live_tup, n_dead_tup
FROM pg_stat_user_tables ORDER BY n_dead_tup DESC;
blogs=# SELECT
pid,
datname,
relid::regclass AS table_name,
phase,
heap_blks_total,
heap_blks_scanned,
heap_blks_vacuumed,
index_vacuum_count,
max_dead_tuples,
num_dead_tuples
FROM pg_stat_progress_vacuum;
blogs=# SELECT
pid,
usename,
query_start,
state,
wait_event_type,
wait_event,
query
FROM pg_stat_activity
WHERE query LIKE 'autovacuum:%';
blogs=# SELECT
relname,
last_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze
FROM pg_stat_user_tables;
Best practices
Conclusion
Autovacuum is one of PostgreSQL's most important self-maintenance features. It quietly removes dead tuples, refreshes planner statistics, prevents table and index bloat, and safeguards the database from transaction ID wraparound. Although the default configuration works well for many workloads, high-traffic systems often benefit from tailored settings at the server or table level.
Regular monitoring of dead tuples, Autovacuum activity, long-running transactions, and freeze age can help you identify issues before they impact performance. By understanding how autovacuum works and tuning it deliberately — is what keeps a PostgreSQL database efficient, responsive, and reliable as it grows.
Talk to the Mafiree DB Team →
Orbit