REST API Multitenancy Patterns: Isolation, Routing, and Scale
A practical guide to REST API multitenancy patterns: routing, isolation, auth, quotas, caching, observability, and deployment—plus concise code examples.
Image used for representation purposes only.
Overview
Multitenancy lets one REST API serve many customers (tenants) securely and efficiently. Getting it right means balancing isolation, operability, and cost. This guide surveys practical patterns for routing, authentication, data isolation, caching, quotas, observability, and deployment—plus concise code snippets you can adapt.
Tenancy models and isolation levels
Choose a data model that matches your growth, regulatory, and operability needs.
-
Database-per-tenant
- Pros: Strong isolation, per-tenant backups/restore, easy data residency, noisy-neighbor control.
- Cons: Operational overhead (migrations, connection limits), cost at small scale.
- Fit: Enterprise tenants, regulated data, explicit SLAs.
-
Schema-per-tenant (shared database)
- Pros: Moderate isolation, simpler ops than DB-per-tenant, easier migrations than thousands of DBs.
- Cons: Still many objects; cross-tenant queries harder; permissions must be airtight.
- Fit: Mid-size SaaS with hundreds–low thousands of tenants.
-
Shared schema with tenant discriminator (tenant_id column + policies)
- Pros: Lowest cost per tenant, elastic scaling, simple to query.
- Cons: Strict guardrails required to prevent leakage; “hot” tenants can impact others.
- Fit: Self-serve SaaS with many small tenants.
-
Hybrid/sharded
- Mix of the above. For example, small tenants use shared schema; large or regulated tenants get dedicated schema/DB in a specific region.
API surface and routing patterns
Expose tenant context consistently at the edge and propagate it internally.
-
Host-based routing (preferred)
- tenant-specific host: https://acme.api.example.com/orders
- Pros: Natural DNS segregation, works well with CDNs and gateway rules.
- Cons: Requires wildcard certs/DNS automation.
-
Path-based routing
- URL pattern: https://api.example.com/t/acme/orders
- Pros: Easy to adopt; single hostname.
- Cons: Caching layers must vary on path segment; some clients hardcode paths.
-
Header-based routing
- Custom header: X-Tenant-Id: acme
- Pros: Clean URLs; flexible internal switching.
- Cons: Easy to omit; poor cache keying by default; must be allowed through proxies.
Recommendation: Pick one canonical pattern (usually host- or path-based). If you accept multiple, normalize to a single resolved tenant identity server-side and use it for all downstream operations.
OpenAPI hint
Define tenant as a parameter so client SDKs handle it consistently.
paths:
/t/{tenantId}/orders:
parameters:
- in: path
name: tenantId
required: true
schema: { type: string }
Authentication and authorization
Identity must be tenant-scoped.
-
Authentication
- OIDC/OAuth2 with JWTs. Include tenant_id (or org_id) and audience claims.
- For first-party service calls, use mTLS or signed service tokens with tenant context.
-
Authorization
- Prefer ABAC/RBAC composed with tenant scope: role=admin, tenant_id=acme.
- Prevent cross-tenant access by failing closed if tenant context is missing.
-
Token scoping examples
- iss: https://auth.example.com
- aud: api.example.com
- sub: user-123
- tenant_id: acme
- roles: [admin]
Enforcing isolation in the data layer
Application checks are not enough—enforce at the database.
- Postgres Row-Level Security (RLS) with session variable
-- One-time setup per table
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id', true));
-- In the request transaction
SET LOCAL app.tenant_id = 'acme';
-- All SELECT/INSERT/UPDATE/DELETE now constrained by the policy
-
Connection management
- Database-per-tenant: keep a directory of DSNs; pool per tenant; limit churn with LRU pools.
- Shared DB: a single pool; set session tenant variable per request/transaction.
-
Encryption
- At-rest: per-tenant KMS keys if compliance requires; otherwise shared key with RLS is common.
- In-flight: TLS everywhere; consider mTLS between services.
-
Migrations
- Use declarative migrations with idempotent steps. For DB-per-tenant, run in waves with health gates. For shared, run once with backfill jobs guarded by RLS.
Edge and gateway concerns
- Normalize tenant early and attach to request context.
- Reject requests lacking tenant context (avoid dangerous defaults).
- Apply per-tenant rate limits and WAF rules.
Example Express middleware:
function resolveTenant(req, _res, next) {
// 1) host-based: acme.api.example.com
const sub = req.hostname.split('.')[0];
// 2) path-based fallback: /t/{tenantId}
const pathTenant = req.path.startsWith('/t/') ? req.path.split('/')[2] : null;
// 3) header as last resort
const headerTenant = req.get('X-Tenant-Id');
req.tenantId = sub !== 'api' ? sub : (pathTenant || headerTenant);
if (!req.tenantId) return next(new Error('Tenant required'));
next();
}
Rate limiting, quotas, and shaping
Protect shared capacity while honoring SLAs.
- Key limits by tenant, and optionally by user or token.
- Implement token bucket or sliding window in a shared store (e.g., Redis) with keys like rl:{tenant}:{route}.
- Support higher tiers with burst+rate settings per plan.
- Expose limits via headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After.
Pseudo-implementation:
key = f"rl:{tenant}:{route}:{epoch_minute()}"
count = redis.incr(key)
if count == 1: redis.expire(key, 65)
if count > limit: return 429
Caching and CDN safety
- Partition caches by tenant to avoid data leakage.
- Example Vary headers: Vary: Authorization, X-Tenant-Id
- For host-based tenancy, the hostname already partitions edge caches.
- Namespace application caches: cache:{tenant}:{resource}:{id}.
- Compute ETags with tenant salt.
Background jobs, events, and webhooks
- Queues
- Add tenant_id to every job payload and queue key. Consider per-tenant priorities.
- Separate DLQs per tenant or at least include tenant metadata for triage.
- Webhooks
- Sign with per-tenant secrets; rotate independently.
- Retry with exponential backoff; cap per-tenant concurrency.
- Event buses
- Route events to tenant-specific topics/partitions when isolation is critical.
Search and indexing
- Shared index with tenant filter
- Lowest cost; enforce tenant filter server-side and in query templates.
- Index-per-tenant
- Stronger isolation; easier delete/restore; more shards to manage.
- Hybrid
- Large tenants get dedicated indexes; small tenants share.
Data residency and regionalization
- Pin tenant to a region at signup; store region in the tenant record.
- Route at the edge based on tenant.region to region-specific backends and databases.
- Keep data processing in-region; replicate only metadata needed for global routing.
- Provide data export/delete per region for compliance.
Observability and SLOs
Instrument every layer with tenant as a first-class label.
- Logging
- Include tenant_id, user_id, request_id. Redact PII by policy.
- Metrics
- Counters: requests_total{tenant,route,status}
- Histograms: latency_seconds{tenant}
- Resource: db_connections{tenant}, queue_lag{tenant}
- Tracing
- Propagate tenant in baggage headers; annotate spans.
- SLOs
- Define per-tier SLOs (availability, latency). Alert on error budget burn by tenant or tier.
Versioning and customization
- API versioning
- Global versions (v1, v2) with deprecation windows.
- Per-tenant pinning to versions during migration periods.
- Feature flags
- Enable by tenant for phased rollout; persist in config service and cache with short TTLs.
- Configuration drift
- Keep a canonical tenant configuration document; track changes and provenance.
Testing and CI/CD for multitenancy
- Contract tests ensure tenant context is required and enforced end-to-end.
- Seed test tenants of each tier/region.
- Run migration smoke tests against representative tenants (small/large, low/high data volume).
- Chaos tests: inject faults scoped to a single tenant to validate blast-radius limits.
Reference architectures
- Monolith with tenant-aware middleware
- Single codebase; strong guardrails: RLS, policy layer, cache namespacing.
- Microservices behind a tenant-aware API gateway
- Gateway resolves tenant and injects headers (X-Tenant-Id) and auth context.
- Services treat tenant as mandatory; DB layer enforces policies.
- Kubernetes
- Namespaces per environment; not per tenant (too many). Use per-tenant labels in telemetry and per-tier HPA settings.
- Sidecars (authz, policy, rate limit) can centralize enforcement.
Example request flow
- Client sends GET https://acme.api.example.com/orders?status=open with OAuth2 token.
- Edge gateway validates token, resolves tenant=acme, applies per-tenant rate limit.
- API sets DB session variable app.tenant_id=acme; RLS policies constrain queries.
- Cache keys include tenant; results served or stored under cache:acme:orders:hash.
- Logs, metrics, and traces include tenant; alerts roll up by tenant and tier.
Common pitfalls and how to avoid them
- Missing tenant defaults
- Never infer a global default tenant. Fail closed if tenant is missing.
- Incomplete authorization
- Check both role and tenant. “Global admin” should be explicitly modeled and audited.
- Cache leaks
- Always vary cache by tenant. Review every shared cache boundary.
- Background job drift
- Ensure every job payload carries tenant_id. Validate in consumer before work starts.
- Unbounded noisy neighbors
- Apply quotas not just at HTTP but also at DB, queue, and worker pools per tenant or tier.
- Migration surprises
- Run canary migrations; measure query plans per large tenant; keep rollback scripts.
A compact checklist
- Canonical tenant resolution (host/path/header) with strict validation
- Auth tokens carry tenant_id; ABAC/RBAC checks include tenant
- Data-layer enforcement (RLS or strict WHERE tenant_id)
- Per-tenant rate limit, quotas, and worker concurrency
- Cache namespacing and CDN Vary rules
- Tenant-labeled logs, metrics, traces
- Region pinning and routing for residency
- Webhooks signed per tenant; DLQs carry tenant context
- Safe migrations and schema evolution strategy
- Automated tests for cross-tenant isolation
Final thoughts
Start with a clear isolation target and pick the simplest model that meets it. Enforce tenant boundaries in multiple layers—identity, routing, data, and observability. As you scale, move hot or sensitive tenants to stronger isolation without rewriting your API contract.
Related Posts
Designing REST APIs with Partial Response Field Selection (fields, select, $select)
Design and implement REST API partial responses for speed and safety—syntax options, caching, security, and implementation patterns with examples.
Designing REST API Batch Operations: Patterns, Semantics, and Examples
Designing robust REST API batch operations: models, atomicity, idempotency, error handling, async jobs, limits, and examples.
GraphQL Schema Design Best Practices: A Practical Guide
Practical best practices for GraphQL schema design: naming, types, pagination, nullability, errors, auth, evolution, and performance.