Core Problem: The toast.* Reloptions Are Fundamentally Broken
PostgreSQL has long supported toast.* reloptions (e.g., toast.vacuum_truncate, toast.autovacuum_vacuum_threshold) that allow independent tuning of VACUUM/autovacuum behavior for a relation's TOAST side-table. The documented contract is simple:
If a table parameter value is set and the equivalent toast. parameter is not, the TOAST table will use the table's parameter value.
Nathan Bossart's investigation — spawned by a separate bug where vacuum_rel() was scribbling on its VacuumParams argument — revealed that this contract is violated in essentially every interesting code path. The problems form a cluster of related defects in both the manual VACUUM path and the autovacuum path:
Defect 1: vacuum_rel() never consults the parent's reloptions when recursing into TOAST
When a user issues VACUUM foo, vacuum_rel() recurses to process foo's TOAST table, but at that point it only reads the TOAST relation's own reloptions. The parent's reloptions are nowhere in scope. The VacuumParams-scribbling bug actually masked this in some cases — parameters had been overridden in-place before recursion, so the TOAST invocation accidentally saw parent-derived values. Fixing the scribble bug exposes the underlying inheritance failure.
Defect 2: Autovacuum treats TOAST options as all-or-nothing
In do_autovacuum() and table_recheck_autovac(), the main table's reloptions are consulted for the TOAST table only when the TOAST table has no reloptions set at all. Setting even a single unrelated toast.* option (e.g., toast.vacuum_index_cleanup = off) causes every other inheritable parameter (e.g., autovacuum_vacuum_threshold) to silently revert to global defaults. This is because TOAST reloptions are parsed as a single StdRdOptions struct; the code has no per-field "is this set?" concept on back branches.
Defect 3: Some parameters are left uninitialized for autovacuum and patched in by vacuum_rel()
Parameters like vacuum_truncate are not populated by autovacuum at all; autovacuum relies on vacuum_rel() to fill them in from reloptions. But since vacuum_rel() itself doesn't consult parent reloptions (Defect 1), this leaves TOAST tables silently using global defaults regardless of what the user set on the parent.
Why This Matters Architecturally
TOAST tables occupy a strange dual position in the system:
- In manual VACUUM, a TOAST table is a secondary citizen — recursed into from the main table's processing with inherited parameters.
- In autovacuum, TOAST tables are first-class work items —
do_autovacuum() enumerates them independently and computes thresholds per-relation.
The reloption inheritance code has to bridge this dichotomy, but it does so incompletely. Michael Paquier correctly characterized this as an "old issue that cannot be solved as long as we rely on the relopts to be an all-or-nothing thing when assigning the individual values for the TOAST relation" — i.e., the underlying representation lacks the bit-level tracking needed to correctly merge parent and child option sets.
The v18 addition of isset_offset in the reloptions machinery partially addresses this for newer versions, but it doesn't exist in v13–v17, making back-patching substantively harder.
Design Alternatives Considered
Option A: Resolve at VACUUM time (Nathan's initial proposal)
Introduce resolve_toast_vac_opts() / resolve_toast_rel_opts() helpers called from autovacuum (table_recheck_autovac, NeedsAutoVacTableForXidWraparound, do_autovacuum) and from vacuum_rel(). Combine parent and TOAST reloptions at use-time, with toast.* winning when both are set.
Tradeoff: Preserves the current storage model. Requires touching every site that reads TOAST reloptions. On back branches, some parameters remain unfixable because there's no way to distinguish "explicitly set to default" from "unset."
Option B: Materialize inheritance at DDL time (proposed by Zhong Yang)
When ALTER TABLE ... SET (foo=...) is executed, propagate the value to the TOAST table's reloptions if the corresponding toast.foo is unset. On CREATE TABLE, inherit parent reloptions into the newly-created TOAST relation.
Fatal flaw (caught by Nathan): RESET becomes ambiguous. Consider:
ALTER TABLE test SET (vacuum_truncate = false);
ALTER TABLE test SET (toast.vacuum_truncate = false);
ALTER TABLE test RESET (vacuum_truncate);
The user expects the TOAST setting to survive. But with eager materialization, both reloptions were stored identically on disk — there's no provenance record distinguishing "inherited copy" from "explicitly set." We'd either need a placeholder sentinel value or a provenance bit, both of which Zhong conceded were "too much work."
Option C: Resolve only in vacuum_rel() (Shayon Mukherjee's proposal)
A narrower fix: when vacuum_rel() is invoked on a TOAST relation and a parameter is VACOPTVALUE_UNSPECIFIED, look up the parent via pg_class.reltoastrelid and inherit. This is appealing because it's localized, doesn't change function signatures, and is potentially back-patchable. However, it doesn't address the autovacuum threshold computation (Defect 2) where the decision of whether to vacuum at all is made before vacuum_rel() is reached.
Option D: Remove toast.* reloptions entirely
Nathan's most radical suggestion, floated after writing the PoC patch and realizing the complexity: "AFAICT any moderately-complicated setup basically doesn't work at all." Informal polling of colleagues suggested negligible field usage. By May 2026 Nathan had written an actual removal patch.
Back-Patching Question
A secondary but important debate: should fixes land on back branches? Michael Paquier argued conservatively for HEAD-only — the bug has existed for years without user complaints, suggesting either low impact or low usage. Nathan initially wanted to back-patch the clearly-fixable subset, but after writing the PoC and confronting the isset_offset availability issue, converged on "fix-on-HEAD-only." Shayon pushed back from a user perspective: the documentation promises inheritance, and users (including himself) are genuinely surprised when it doesn't work.
Testing Challenges
Michael suggested verification via DEBUG1 log scraping combined with aggressive autovacuum spawn rates to confirm the effective options used per relation. Nathan dismissed this as "a recipe for a flaky test" — autovacuum timing-dependent tests are notoriously unstable in the buildfarm. No clean testing strategy emerged in the thread, which is itself a signal about how awkward this area of the code is.
Implications
The eventual trajectory — a patch to remove toast.* reloptions — would be a notable simplification. It would:
- Eliminate the
table_toast_map in autovacuum (used to correlate TOAST processing with parent-table thresholds).
- Remove a significant chunk of reloption parsing machinery.
- Mean TOAST tables simply always inherit from their parent, full stop.
The counterargument (not strongly voiced in this thread) is that some workloads genuinely want asymmetric tuning — e.g., more aggressive vacuum on the TOAST side when large-object churn dominates. But if the feature has been silently broken for years without complaint, the argument that anyone actually relies on it is weak.