Your table is fine.
Your autovacuum settings are the ones Postgres shipped in 2005.

robovac reads a statistics snapshot of one table and tells you three things: what autovacuum is doing today, what it should do at your write rate, and the exact ALTER TABLE that closes the gap. Every term in the report links to a page that explains it with a demo you can drag.

No account, no agent required, no write access. It never connects to your database, you bring the numbers.

READS
1 table · statistics only
TUNES
15 settings · trigger, cost, freeze
EXPLAINS
34 terms · with live demos
STORES
nothing

START HERE

run it, paste it, read it
01 — RUN THIS, ANYWHERE YOU HAVE psql
SELECT
 current_database() AS db,
 s.schemaname AS schema_name,
 s.relname AS table_name,
 c.relpages::bigint AS relpages,
 c.relallvisible::bigint AS relallvisible,
 c.reltuples::bigint AS reltuples,
 s.n_live_tup AS n_live_tup,
 s.n_dead_tup AS n_dead_tup,
 s.n_tup_ins AS n_tup_ins,
 s.n_tup_upd AS n_tup_upd,
 s.n_tup_del AS n_tup_del,
 s.n_tup_hot_upd AS n_tup_hot_upd,
 s.last_autovacuum::text AS last_autovacuum,
 s.last_vacuum::text AS last_vacuum,
 s.n_mod_since_analyze AS n_mod_since_analyze,
 age(c.relfrozenxid) AS xid_age,
 mxid_age(c.relminmxid) AS mxid_age,
 (SELECT count(*) FROM pg_index i WHERE i.indrelid = c.oid) AS index_count,
 c.reloptions AS reloptions,
 c.relispartition AS is_partition,
 (c.reltoastrelid <> 0) AS has_toast,
 current_setting('server_version_num')::int AS version_num,
 pg_is_in_recovery() AS is_replica,
 -- A platform that already meters vacuum against live load makes the cost
 -- advice redundant. missing_ok = true returns NULL on vanilla Postgres,
 -- so this probes a capability without a vendor list.
 current_setting('enable_google_adaptive_autovacuum', true) AS adaptive_vacuum,
 -- xmax is the next xid to be assigned. Its delta over the two runs is the
 -- xid consumption rate. (xmin is the oldest running transaction, so using
 -- it here would read the horizon's movement instead, and would report a
 -- rate near zero exactly when a long transaction pins it.)
 pg_snapshot_xmax(pg_current_snapshot())::text::numeric AS xid_now,
 -- How far behind the oldest snapshot sits, in xids. This is what makes
 -- the dead-but-not-removable floor computable.
 (pg_snapshot_xmax(pg_current_snapshot())::text::numeric
 - pg_snapshot_xmin(pg_current_snapshot())::text::numeric) AS horizon_xids,
 -- The ProcArray snapshot above misses replication slots, and a stuck slot
 -- is the classic cause of a horizon that never advances.
 (SELECT max(GREATEST(age(s.xmin), age(s.catalog_xmin)))
 FROM pg_replication_slots s) AS slot_horizon_xids,
 (SELECT jsonb_object_agg(name, setting) FROM pg_settings
 WHERE name IN ('autovacuum_vacuum_scale_factor', 'autovacuum_vacuum_threshold', 'autovacuum_vacuum_insert_scale_factor', 'autovacuum_vacuum_insert_threshold', 'autovacuum_analyze_scale_factor', 'autovacuum_analyze_threshold', 'autovacuum_vacuum_cost_delay', 'autovacuum_vacuum_cost_limit', 'vacuum_cost_page_hit', 'vacuum_cost_page_miss', 'vacuum_cost_page_dirty', 'vacuum_freeze_min_age', 'vacuum_freeze_table_age', 'autovacuum_freeze_max_age', 'autovacuum_multixact_freeze_max_age', 'vacuum_cost_limit', 'vacuum_cost_delay', 'vacuum_failsafe_age')) AS global_settings,
 now()::text AS captured_at
FROM pg_stat_user_tables s
JOIN pg_class c ON c.oid = s.relid
WHERE s.schemaname = 'schema' AND s.relname = 'table'
Change the schema and table name, and run it twice, 30-60 s apart, for real rates. It reads statistics catalogues only: no table data, no locks, safe on a primary.
02 — PASTE THE OUTPUT
or open a demo report
Two result blocks (the query run twice) give real rates; one block builds a report that says which figures are unknown.
03 — OR LET YOUR AGENT DO IT

Register the MCP server once and ask in plain language. If your agent already reaches Postgres, it runs the query itself and hands you back a link.

WHAT THE REPORT TELLS YOU
How often vacuum actually fires

Your trigger threshold against your write rate, in days, not percentages. Most large tables discover here that the answer is "every three weeks".

How close the freeze horizon is

Table age against autovacuum_freeze_max_age and the wraparound limit, with the date the cluster would stop accepting writes.

What one vacuum pass costs

Duration and throughput under your cost settings. A 20 ms delay on a 14 GB table is 44 minutes of throttled I/O per run.

The statement to run

An ALTER TABLE … SET (…) that mirrors the sliders, listing only what you changed. You run it; robovac cannot.

IF YOU ARE JUST HERE TO LEARNall 34 ↗
1robovac has no database driver. The query is yours to run and the output is yours to paste. The report you build here is computed in your browser and stored nowhere: its URL carries the whole snapshot in the fragment. Through the MCP server it works differently. create_report also stores the report for 30 days and returns a short link to it, next to that same permalink.
2Proposals are arithmetic, not advice: trigger thresholds from your row counts, vacuum duration from the cost model, freeze margins from your xid rate. Every figure on the report shows the formula behind it in a footnote, and every setting page shows the same formula with a slider attached.
robovac — Postgres vacuum settings, explained and tuned