← Start of tutorial

WIUNIX ACADEMY

PostgreSQL autovacuum: diagnose cleanup lag and wraparound risk

Page 3 of 3. Review the earlier steps and prerequisites before continuing.

YOUR SYSTEM. YOUR STEPS.

Where are you working?

Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.

AlmaLinux 10.2 — existing clientReviewed against official documentation ·

USE YOUR OWN VALUES

Configure this tutorial

Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.

Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.

Set values for my environment4 editable values
11

LET’S BUILD

Optionally freeze only the approved relation

Run this panel in the maintenance session with autocommit on, outside an explicit transaction. Verify the displayed destination before continuing. FREEZE requests aggressive freezing; it cannot remove the need to respect old snapshots. TRUNCATE FALSE avoids the optional end-of-table truncation lock and does not reclaim the file’s tail for the OS.

The sample timeouts bound this planned intervention; they are not an emergency recovery recipe. The operation can still generate substantial I/O and WAL. Watch it from the observation session with the progress panel. Treat a permission warning or skipped relation as a failure to perform the requested maintenance: VACUUM can skip an unauthorized table and still return. The gate also requires MAINTAIN and a non-temporary application relation outside system schemas. After execution, query the ages again, including TOAST, rather than merely checking last_vacuum.

ONLY keeps inherited child tables outside this requested maintenance; the relation's own TOAST processing remains enabled by default.

sql

Second, approved maintenance psql session

\conninfo
SELECT current_database(),current_user,pg_is_in_recovery();
\set target_schema 'public'
\set target_table 'orders'
SELECT current_setting('server_version_num')::integer BETWEEN 180000 AND 189999
       AND NOT pg_is_in_recovery()
       AND EXISTS (
         SELECT FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
         WHERE n.nspname=:'target_schema' AND c.relname=:'target_table'
           AND c.relkind IN ('r','m')
           AND c.relpersistence <> 't'
           AND n.nspname !~ '^pg_' AND n.nspname <> 'information_schema'
           AND has_table_privilege(current_user,c.oid,'MAINTAIN')
       ) AS target_ready \gset
\if :target_ready
\else
\echo 'STOP: require the approved PostgreSQL 18 primary, a non-temporary application table/materialized view and MAINTAIN privilege.'
\quit
\endif
SET lock_timeout='3s';
SET statement_timeout='15min';
VACUUM (VERBOSE, FREEZE, TRUNCATE FALSE) ONLY :"target_schema".:"target_table";
\quit
How do I know it worked?

The authorized application relation completes or reports a specific error/warning; the maintenance session closes without changing global settings. A skipped relation is not a successful maintenance result.

Follow the failed boundary

Run these diagnostic panels in the original read-only observation session. Reopen that session if you have closed it. Keep connection details and results private; grant changes, cancellations and replication changes need their own owner-approved decisions.

Permission error or missing activity details

Missing pg_read_all_stats can hide other sessions. Table maintenance needs the table owner or the appropriate MAINTAIN privilege; do not solve this by granting broad superuser access. Inspect the existing grants and ask the DBA to perform or delegate the exact action.

sql
SELECT current_user,pg_has_role(current_user,'pg_read_all_stats','USAGE') AS can_read_all_stats;
SELECT c.oid::regclass,c.relowner::regrole,
       has_table_privilege(current_user,c.oid,'MAINTAIN') AS has_maintain
FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
WHERE n.nspname='public' AND c.relname='orders';
Dead-row estimates or timestamps do not move

A cached statistics snapshot or delayed statistics publication can mislead. Clear only this session’s snapshot, inspect reset time and resample after a reasonable interval. This does not reset shared counters. Autovacuum completion may legitimately race the observation.

sql
SELECT pg_stat_clear_snapshot();
SELECT clock_timestamp(),stats_reset FROM pg_stat_database
WHERE datname=current_database();
SELECT schemaname,relname,n_dead_tup,last_autovacuum,last_vacuum
FROM pg_stat_user_tables
WHERE schemaname='public' AND relname='orders';
The manual action waits or hits lock_timeout

A timeout leaves earlier completed vacuum work useful but does not prove that freezing finished. Reinspect phase and lock holders; schedule around competing DDL. Do not retry in a tight loop or terminate every reported blocker.

sql
SELECT p.pid,p.datname,p.relid,p.phase,
       p.heap_blks_total,p.heap_blks_scanned,p.heap_blks_vacuumed,
       p.index_vacuum_count,a.backend_type,a.wait_event_type,a.wait_event,
       pg_blocking_pids(p.pid) AS blocking_pids
FROM pg_stat_progress_vacuum p
LEFT JOIN pg_stat_activity a USING(pid)
WHERE p.datid=(SELECT oid FROM pg_database WHERE datname=current_database())
ORDER BY p.pid;
Freeze age stays high after a successful return

Repeat the exact target query, including its TOAST ages, then revisit retained horizons and other old relations. Vacuum cannot freeze tuples newer than the safe horizon. An old database age can belong to another object even when this table improved.

sql
\set target_schema 'public'
\set target_table 'orders'
SELECT c.oid,c.relkind,n.nspname,c.relname,c.reloptions,
       age(c.relfrozenxid) AS xid_age,mxid_age(c.relminmxid) AS mxid_age,
       age(t.relfrozenxid) AS toast_xid_age,
       mxid_age(t.relminmxid) AS toast_mxid_age
FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
LEFT JOIN pg_class t ON t.oid=c.reltoastrelid
WHERE n.nspname=:'target_schema' AND c.relname=:'target_table';
SELECT pid,usename,application_name,backend_type,state,
       clock_timestamp()-xact_start AS transaction_duration,
       age(backend_xid) AS xid_age,age(backend_xmin) AS xmin_age,
       wait_event_type,wait_event
FROM pg_stat_activity
WHERE backend_xid IS NOT NULL OR backend_xmin IS NOT NULL
ORDER BY age(backend_xmin) DESC NULLS LAST LIMIT 30;
SELECT gid,prepared,owner,database,age(transaction) AS xid_age
FROM pg_prepared_xacts ORDER BY age(transaction) DESC;
SELECT slot_name,slot_type,database,active,active_pid,
       age(xmin) AS xmin_age,age(catalog_xmin) AS catalog_xmin_age,
       wal_status
FROM pg_replication_slots ORDER BY age(catalog_xmin) DESC NULLS LAST;

Recover without making the horizon problem worse

If the planned manual vacuum must stop, use the client’s interrupt in that maintenance terminal and let PostgreSQL finish cancellation. Vacuum is incremental: completed cleanup is not rolled back like a transaction, and there is no “unfreeze” recovery step. Keep autovacuum enabled, preserve the error, identify the cause and reassess the age/impact before scheduling another attempt.

When PostgreSQL reports remaining transactions before wraparound, or refuses new XID assignment, escalate immediately. The DBA must stop avoidable write pressure, resolve legitimate horizon blockers and vacuum the affected databases with the privileges needed for catalogs. The single-table, 15-minute panel is deliberately not that runbook. Do not restart repeatedly, run VACUUM FULL, erase transaction-state files or manipulate catalog ages.

For prevention, measure how quickly each horizon grows and whether workers can keep up. Tune thresholds/cost budgets for the affected workload with a recorded previous configuration and one reversible change at a time. Lowering a table’s cleanup threshold can help churn; it cannot release a retained snapshot. More workers can increase I/O pressure. Validate the resulting age trend and application latency rather than adopting a universal tuning number.

12

LET’S BUILD

Close observation and retain a useful decision

Repeat the database-age and exact-target panels after any maintenance, then close the read-only session. Keep a redacted note with database/relation identities, timestamps, both age series, horizon holders, vacuum outcome and the next owner/action.

Decide explicitly: normal cleanup observation, a planned table-maintenance change, or an urgent database-wide incident. None of these diagnostics proves a backup restore, an operating-system installation test or guaranteed elimination of future wraparound risk.

sql

Persistent observation psql session

\quit
How do I know it worked?

The diagnostic session is closed and the maintenance decision has evidence, ownership and a next check.

GO TO THE SOURCE

Official references

Each environment states how it was reviewed. A documentation review does not claim a hands-on test of every version.

KEEP LEARNING

Your next step

Explore all tutorials ↗