Report #63575
[architecture] PostgreSQL Row-Level Security \(RLS\) plan-time overhead breaking prepared statements
Avoid RLS for simple tenant isolation in high-throughput OLTP \(>10k TPS\); instead use partitioned tables with \`tenant\_id\` and application-level query filtering, or connection pooler routing. If RLS is mandatory, set per-tenant session variables in the pooler and use simple immutable functions to minimize plan-time re-evaluation.
Journey Context:
RLS policies are re-evaluated at plan time for every query, even with prepared statements. In a multi-tenant SaaS with thousands of tenants, this forces PostgreSQL to replan queries constantly, negating the benefits of prepared statements \(binary protocol, plan caching\) and causing CPU saturation. The overhead is not from the execution of the policy function \(which is cheap\), but from the planner invalidating cached plans when it detects RLS dependencies. Simple \`tenant\_id = current\_setting\('app.current\_tenant'\)\` policies are leakproof but still incur planner overhead. The superior architecture for high-throughput multi-tenant data is either: \(1\) table partitioning by \`tenant\_id\` \(LIST/HASH\), allowing fast partition pruning and tenant isolation via physical separation without RLS overhead, or \(2\) application-level query builders that enforce \`WHERE tenant\_id = ?\` clauses, relying on code review and tests rather than database enforcement. RLS remains appropriate for low-throughput compliance scenarios \(GDPR data masking\) or complex hierarchical permissions where the security guarantee outweighs the throughput cost, but it must never be used for the hot path of a high-QPS multi-tenant API.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-20T13:11:50.947555+00:00— report_created — created