title: Postgres VACUUM + ANALYZE schedule description: When to run VACUUM and ANALYZE manually on top of Postgres autovacuum. Mostly: after big bulk loads. status: active tags: - runbook - backend
Postgres VACUUM + ANALYZE schedule¶
Postgres autovacuum handles 95% of cases. The remaining 5% is when our scraper or affiliate-reconciliation work bulk-inserts thousands of rows in a tight window — autovacuum waits for thresholds, the planner ends up working off stale stats, and queries get slow.
Manual ANALYZE — when¶
Run after:
- npm run scrape completes when listing count delta > 500 rows. A full scrape adds ~2,800 listings; the planner needs to know.
- Affiliate-reconciliation CSV import (#17) — bulk insert into Click and (eventually) AffiliateConversion.
- Match-confidence threshold tuning (#146) — bulk update of Listing.matchStatus.
Or scoped:
Cheap (seconds even on a populated DB). Always safe.
Manual VACUUM — when¶
Autovacuum should be enough for everything we write today. Manual VACUUM becomes useful only if:
- A long-running transaction blocked autovacuum and bloat accumulated. Symptom: a previously-fast query now slow on pg_stat_user_tables.n_dead_tup > 20% of n_live_tup.
- A table is approaching XID wraparound (~1.5B transactions). Diagnostic: SELECT datname, age(datfrozenxid) FROM pg_database;. Hit ~1B → manual VACUUM FREEZE.
Daily lifecycle on hobby scale: don't manually VACUUM. Trust autovacuum.
Automated schedule (M2 onwards)¶
Per #67 once pg-boss runs, the post-scrape ANALYZE becomes a chained job:
// Sketch — lands when #67 ships.
export async function postScrape(): Promise<void> {
await runScrapers();
await db.$executeRaw`ANALYZE "Listing", "ListingPrice"`;
}
For now, the manual command above runs at the end of npm run scrape if listing count jumped meaningfully (the script already counts delta). Add the db.$executeRaw('ANALYZE') line in scripts/run-scrapers.ts when prod telemetry shows it's worth the additional step.
Neon-specific¶
Neon manages autovacuum aggressively (it's their cost surface — every undead tuple is a row they'd otherwise have to bill for). You shouldn't need manual VACUUM on Neon. Manual ANALYZE still helps the planner.