PostgreSQL VACUUM, Bloat, and Autovacuum Tuning
Fast recall
Quick revision
Core answer
autovacuum_vacuum_scale_factor, raise cost limits, and monitor pg_stat_user_tables for n_dead_tup and last_autovacuum. Long-running transactions block dead-tuple cleanup and are the single most common cause of runaway bloat.Whether you understand MVCC's cost (dead tuples), the difference between VACUUM, VACUUM FULL, and ANALYZE, how autovacuum decides when to run, and the most common real-world cause of bloat (long transactions holding back the xmin horizon).
Running `VACUUM FULL` on a production table to 'fix bloat' without realising it takes an ACCESS EXCLUSIVE lock and rewrites the table — or ignoring autovacuum entirely and discovering 80% bloat weeks later.
Explain the xmin horizon, `pg_stat_user_tables.n_dead_tup`, the `autovacuum_vacuum_scale_factor` per-table override for hot tables, `pg_repack` as the online alternative to VACUUM FULL, and transaction ID wraparound as the existential threat.
Interview-ready guide
Interview answer
PostgreSQL uses MVCC, so every UPDATE and DELETE leaves an old row version behind. Those dead tuples live until a transaction no longer needs them and VACUUM reclaims the space.
Three different operations
- VACUUM marks dead space reclaimable in the free-space map without returning it to the OS; takes a SHARE UPDATE EXCLUSIVE lock so reads and writes continue
- VACUUM FULL rewrites the entire table into a new file and returns space to the OS — but it takes ACCESS EXCLUSIVE and blocks everything, so it is not a production tool
- ANALYZE refreshes planner statistics; cheap and frequent
Autovacuum
- Runs VACUUM and ANALYZE automatically
- Fires when dead tuples exceed a threshold:
autovacuum_vacuum_threshold + scale_factor * reltuples - Default scale factor is 0.2, which is far too conservative on hot tables — I override to 0.01 or 0.02 per table for queue-like or frequently updated tables
- On modern SSDs the default
autovacuum_vacuum_cost_limitof 200 is too low; I raise it to 1000–2000 so autovacuum can keep up
The real cause of bloat
- Long-running transactions hold back the xmin horizon — VACUUM cannot reclaim any row version possibly visible to them
- Before tuning thresholds I always check
pg_stat_activityfor sessions withxact_startolder than a few minutes, and I configureidle_in_transaction_session_timeoutas a safety net
Repairing existing bloat
- Regular VACUUM will not shrink a 200 GB bloated table back to 80 GB — it only marks space reusable
pg_repackrebuilds tables online with minimal lockingREINDEX CONCURRENTLYhandles bloated indexesVACUUM FULLonly in scheduled downtime
The existential risk
- Transaction ID wraparound — monitor
age(datfrozenxid)so autovacuum's freeze pass keeps up; falling behind forces PostgreSQL to stop accepting writes
PostgreSQL VACUUM, Bloat, and Autovacuum Tuning
Direct Answer
PostgreSQL's MVCC leaves dead tuples behind after every UPDATE and DELETE. VACUUM reclaims that space back to the table's free-space map; VACUUM FULL rewrites the table and returns space to the OS; ANALYZE refreshes planner statistics. Autovacuum runs both automatically but the defaults are conservative — on write-heavy tables you must lower autovacuum_vacuum_scale_factor, raise cost limits, and monitor pg_stat_user_tables for n_dead_tup and last_autovacuum. Long-running transactions block dead-tuple cleanup and are the single most common cause of runaway bloat.
Code Example
-- 1. Find bloated tables and stale autovacuum timestamps
SELECT relname,
n_live_tup,
n_dead_tup,
ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS dead_pct,
last_vacuum, last_autovacuum,
last_analyze, last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;
-- 2. Find the long-running transaction blocking cleanup
SELECT pid, usename, state,
now() - xact_start AS xact_age,
now() - query_start AS query_age,
wait_event_type, wait_event,
query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_age DESC
LIMIT 10;
-- 3. Global safety net — kill idle transactions that hold xmin back
ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s';
ALTER SYSTEM SET statement_timeout = '30s';
SELECT pg_reload_conf();
-- 4. Manual VACUUM with live progress (PG 12+)
VACUUM (VERBOSE, ANALYZE) orders;
SELECT * FROM pg_stat_progress_vacuum;
-- 5. Per-table autovacuum tuning for hot tables
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.02, -- vacuum after 2% dead vs 20% default
autovacuum_vacuum_threshold = 1000,
autovacuum_analyze_scale_factor = 0.01,
autovacuum_vacuum_cost_limit = 2000, -- let autovacuum do more work per round
autovacuum_vacuum_cost_delay = 2 -- ms
);
-- 6. Global autovacuum knobs worth revisiting (postgresql.conf)
-- autovacuum_max_workers = 6 -- parallel autovacuum processes
-- autovacuum_naptime = 30s -- check interval
-- maintenance_work_mem = 1GB -- bigger batches per worker
-- autovacuum_vacuum_cost_limit = 2000 -- default 200 is far too low
-- autovacuum_freeze_max_age = 500M -- when anti-wraparound vacuum triggers
-- 7. Online rebuild when a table is genuinely bloated
-- pg_repack (extension + binary) rewrites the table with minimal locking
-- pg_repack -d app -t public.orders --jobs=4
-- 8. Index bloat (separate from table bloat)
REINDEX INDEX CONCURRENTLY idx_orders_user_created;
-- 9. Wraparound monitoring — the existential threat
SELECT datname, age(datfrozenxid) AS xid_age,
ROUND(100.0 * age(datfrozenxid) / 2000000000, 1) AS pct_until_wraparound
FROM pg_database
ORDER BY xid_age DESC;
-- 10. Emergency: manual anti-wraparound vacuum
VACUUM (FREEZE, VERBOSE, ANALYZE) orders;
Common Mistakes & Pitfalls
Running VACUUM FULL on a production table to 'fix bloat' without realising it takes an ACCESS EXCLUSIVE lock and rewrites the table — or ignoring autovacuum entirely and discovering 80% bloat weeks later.
Check your understanding
Answer these without looking back at the article.
- Why does a VACUUM not shrink a bloated table's on-disk size, and what do you use instead in production?
- How do long-running transactions prevent VACUUM from reclaiming dead tuples?
- What per-table autovacuum overrides would you set for a very hot UPDATE-heavy table?
- What is transaction ID wraparound and how do you monitor for it?
Related interview prep categories
1Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.