Add plan_filter.transaction_cost_limit (issue #4) - #8
Merged
Conversation
Caps the cumulative estimated plan cost of the statements executed in one transaction, so a batch interface cannot smuggle unbounded work through many individually-cheap statements. Requested for throttling PostgREST batch endpoints and mapping the error to HTTP 429. - New PGC_SUSET GUC plan_filter.transaction_cost_limit, default 0 (off), same SQLSTATE (54001) as the statement limit - Accounting happens in a new ExecutorStart hook, not the planner hook, so every execution is charged: a PREPARE/EXECUTE loop over a cached generic plan cannot evade a plan-time accumulator - A transaction callback zeroes the accumulator on commit/abort/prepare; ROLLBACK TO SAVEPOINT does not refund cost already charged - Plain EXPLAIN and parallel workers are exempt; filter_select_only applies as it does to the statement limit - Regression tests cover the reset, no-refund, generic-plan, EXPLAIN, filter_select_only, and SQLSTATE behaviors - README, ADR 0004, and ARCHITECTURE.md document the design, and warn that plan-cost limits are not a security boundary against a hostile role: the cost estimate is computed from USERSET planner GUCs a client can lower to evade either limit
Xof
added a commit
that referenced
this pull request
Jul 21, 2026
Bring transaction_cost_limit into master (re-target of #8)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4.
Summary
Adds
plan_filter.transaction_cost_limit: a cap on the cumulative estimated plan cost of the statements executed within one transaction, so a batch interface can't smuggle unbounded work through many individually-cheap statements. Requested by @steve-chavez for throttling PostgREST batch endpoints, with the error mapped to HTTP 429.The design decision: execution-time accounting
Cost is accumulated in a new
ExecutorStarthook, not the existing planner hook. This is the load-bearing choice: repeatedEXECUTEof a prepared statement stops invoking the planner once a generic plan is cached, so aPREPARE+EXECUTEloop would defeat a plan-time accumulator — exactly the misbehaving-client threat the feature targets. Every execution passes throughExecutorStart. The existing per-statement planner-hook check is untouched. Full rationale in ADR 0004.Details:
PGC_SUSETGUC, default 0 (off), same SQLSTATE54001as the statement limit so one client-side mapping catches bothROLLBACK TO SAVEPOINTdeliberately does not refund cost (the work was attempted)EXPLAINand parallel workers are exempt;filter_select_onlyapplies as it does to the statement limitSecurity caveat (please read)
An adversarial review surfaced that neither cost limit is a hard security boundary against a hostile role. The charged value is the planner's estimate, computed from cost GUCs (
seq_page_cost,cpu_tuple_cost, the parallelism costs) that are allUSERSET— a role that can runSETcan drive its own estimates to zero and evade either limit. This is inherent to plan-cost-based limiting and shared withstatement_cost_limit; it can't be closed from inside the hook. The feature is a cooperative guard against careless load, best kept as one layer behind an application-level limit. This is now documented prominently in the README "Warnings" section, ADR 0004, and ARCHITECTURE.md rather than left implicit — it changes the security story the issue implied, so worth a look before merge.Testing
Regression suite extended (behavioral, one version-independent expected file) covering: per-statement pass / cumulative trip, reset after commit and after abort, savepoint no-refund, the
force_generic_plandiscriminator (fails under plan-time accounting),EXPLAINcharging nothing (with a trailing charged statement so a charge-without-check regression is detectable),filter_select_onlyexemption in both directions, and a symbolic assertion that both limits raise SQLSTATE54001(the PostgREST contract).Verified warning-free build +
installcheckon PostgreSQL 14-18 in anubuntu:24.04rehearsal of the CI steps — each version against both a pristine cluster and one preconfigured withsession_preload_librariesand both limits set — plus cppcheck clean on the new code.