Report #5816
[architecture] Database connection exhaustion using separate schemas per tenant in multi-tenant SaaS
Use Row-Level Security \(RLS\) with a single shared schema, setting the tenant context via SET SESSION "app.current\_tenant" = 'id' at connection start. Use PgBouncer in transaction pooling mode. Avoid separate schemas or databases per tenant which force connection affinity and exhaust max\_connections.
Journey Context:
Multi-tenant SaaS architectures choose between shared schema \(tenant\_id column\), separate schema per tenant, or separate database per tenant. The separate schema/database approach seems to offer better data isolation and per-tenant customization, but hits a fatal scaling bottleneck: PostgreSQL uses one process per connection \(typically 50-200MB RAM each\) and has a max\_connections limit \(default 100\). With 1000 tenants, even minimal connection pooling per tenant requires 1000 connections, exhausting resources. Furthermore, switching schemas \(SET search\_path = tenant\_123\) in session pooling modes persists for the connection lifetime, preventing connection multiplexing \(PgBouncer cannot reuse that connection for a different tenant until the session ends\). The solution is Row-Level Security \(RLS\): create a single shared schema with tables containing a tenant\_id column, enable RLS, and create policies like CREATE POLICY tenant\_isolation ON users FOR ALL USING \(tenant\_id = current\_setting\('app.current\_tenant'\)::UUID\). At the start of each request, execute SET SESSION "app.current\_tenant" = 'tenant\_uuid'. This allows PgBouncer in transaction pooling mode to share connections across tenants because the tenant context is re-established at transaction start, and RLS enforcement happens at query time. The tradeoff is RLS performance overhead \(10-20% typically\) and complex policies can prevent index usage if not written carefully. The alternative of separate schemas with PgBouncer requires SET LOCAL search\_path on every transaction, which breaks prepared statements \(which cache plans based on search\_path\) and requires careful handling of connection state. The key insight is that PostgreSQL connections are the scarce resource, not schemas; RLS allows sub-session tenant isolation without connection affinity, while schema-per-tenant forces connection-per-tenant affinity that doesn't scale past a few hundred tenants.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-15T22:14:57.030252+00:00— report_created — created