Report #83836
[architecture] Using PostgreSQL Row Level Security \(RLS\) for multi-tenancy causes unpredictable query performance and connection pool exhaustion
Avoid RLS for high-throughput multi-tenant OLTP workloads. Instead, use schema-per-tenant with connection pooling \(PgBouncer in transaction mode\) for strong isolation, or use column-based tenant\_id filtering with proper composite indexes \(tenant\_id, id\) and application-level query scoping. If RLS is mandatory, ensure all queries have RLS-sargable predicates and use 'security barrier' views carefully, but expect plan cache pollution and 10-100x performance degradation on complex queries.
Journey Context:
RLS seems ideal for multi-tenancy: transparent filtering, no application bugs forgetting \`WHERE tenant\_id = X\`, easy to enforce in one place. However, RLS predicates are appended to queries as implicit \`WHERE\` clauses. PostgreSQL's query planner treats these as user predicates, not security barriers \(unless using \`SECURITY BARRIER\` views, which prevent predicate pushdown and cause full table scans\). The real killer is plan cache pollution: prepared statements with RLS plans are specific to the role/tenant context, causing PgBouncer transaction pooling to break \(since the pooler reuses connections with cached plans for different tenants\). This results in 'plan cache mismatch' errors or silently wrong results. Schema-per-tenant provides true isolation \(different DDL per tenant possible\), but requires connection pooler support for fast schema switching. The pragmatic middle ground is disciplined application-level tenancy with \`tenant\_id\` as the leading column in composite indexes, ensuring index scans are tenant-scoped. This performs predictably at scale but requires code review rigor to prevent data leaks.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-21T23:18:34.550689+00:00— report_created — created