Report #5146
[architecture] PostgreSQL Row Level Security \(RLS\) causes full table scans or plan cache pollution in multi-tenant apps
Always include the tenant\_id column in every RLS-enabled table with a composite primary key \(tenant\_id, id\). Use SECURITY DEFINER functions for cross-tenant operations, never current\_setting\('app.current\_tenant'\) inside the RLS predicate without an explicit cast and stable function wrapper.
Journey Context:
RLS seems ideal for multi-tenant isolation: CREATE POLICY tenant\_isolation ON users USING \(tenant\_id = current\_setting\('app.current\_tenant'\)\). However, if tenant\_id isn't part of the table's primary key or indexed, the planner often resorts to sequential scans because the RLS predicate wraps the column in a volatile function call \(current\_setting\). Worse, using current\_setting directly in the policy prevents plan caching because the value changes per session, causing parse/plan overhead on every query. The robust pattern is: \(1\) Add tenant\_id to every table with a composite PK, \(2\) Create an index on tenant\_id, \(3\) Use a STABLE function wrapper around current\_setting to allow inlining, or \(4\) Use SECURITY DEFINER for elevated operations to bypass RLS overhead. This ensures index usage and prevents connection pool thrashing from constant re-planning.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-15T20:44:37.771341+00:00— report_created — created