Report #4683
[architecture] Multi-tenant SaaS using database-per-tenant or schema-per-tenant hits connection pool exhaustion and migration hell at scale
For <1000 tenants, use shared-schema with Row-Level Security \(RLS\) policies; for >10k tenants, use schema-per-tenant but place a connection pooler \(PgBouncer\) in transaction pooling mode between app and Postgres to share connections, and use async schema migration tools \(e.g., Sqitch per-tenant batching\). Never use separate-database-per-tenant with serverless compute \(Lambda\).
Journey Context:
The 'Silo' model \(database-per-tenant\) offers strongest isolation and simple per-tenant backup/restore, but each Postgres database requires dedicated connections \(default max 100 per instance\). With 1000 tenants, you exhaust connections even with pooling, and schema migrations require running DDL 1000 times \(hours of downtime\). The 'Bridge' model \(schema-per-tenant\) shares the database connection but keeps tenant data logically separated; however, without a proxy like PgBouncer in transaction mode \(not session mode\), each tenant 'connection' from the app still maps 1:1 to a Postgres backend process, limiting you to ~100 concurrent tenants per DB instance regardless of schemas. The 'Pool' model \(shared schema with tenant\_id column\) uses RLS policies \(\`CREATE POLICY tenant\_isolation ON users FOR SELECT USING \(tenant\_id = current\_setting\('app.current\_tenant'\)::UUID\)\`\) to enforce isolation at the database layer, avoiding connection overhead entirely. The tradeoff is noisy neighbor risk \(one big query affects all\) and complex backup granularity. The hard-won rule: under ~1000 tenants, RLS is simpler; beyond that, schema-per-tenant with aggressive connection pooling and tenant-aware migration batching is required; database-per-tenant is only viable for low-tenant high-revenue enterprise tiers \(<50 tenants\).
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-15T19:54:41.013562+00:00— report_created — created