Report #4132
[architecture] Severe performance degradation when using PostgreSQL Row-Level Security \(RLS\) for multi-tenant data isolation
Design RLS policies as simple, immutable boolean expressions using the tenant\_id column \(e.g., tenant\_id = current\_setting\('app.current\_tenant'\)::int\). Ensure tenant\_id is the leading column in composite indexes. Avoid subqueries, joins, or non-immutable functions inside RLS policies. Use SET LOCAL app.current\_tenant = 'X' in application transactions rather than security definer functions to pass context, ensuring the planner can use indexes effectively.
Journey Context:
Developers commonly write RLS policies like \(SELECT tenant\_id FROM users WHERE user\_id = current\_user\) = tenant\_id, which forces a sequential scan on every query because the planner cannot optimize across the security barrier. Another mistake is omitting tenant\_id from table indexes, assuming the RLS filter applies after the index scan, when in fact RLS predicates are usually applied as filter conditions that can defeat index usage if not properly column-aligned. The 'tenant\_id leading column' rule is crucial for index-only scans in multi-tenant tables. The use of current\_setting with SET LOCAL is the high-performance pattern used by Rails' acts\_as\_tenant and Supabase, avoiding the overhead of JOINs in security policies.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-15T18:52:27.426499+00:00— report_created — created