Chapter 73: Multi-Tenancy Architecture
Multi-tenancy is the architectural approach where a single system serves multiple customers (tenants), each isolated from the others. It's fundamental to SaaS platforms like Salesforce, Slack, and Shopify. The core challenge: balance cost efficiency (shared resources) with isolation (security, performance, customization). This chapter covers isolation models, resource management, data security, and scaling strategies.
Isolation Models
There are four main isolation models, each with different trade-offs between cost, security, and operational complexity.
1. Shared Everything (Pool Model)
graph TB
subgraph "Single Application Instance"
App[Application Server]
end
subgraph "Single Database"
DB[(Database)]
T1[Tenant A data
tenant_id = 'A']
T2[Tenant B data
tenant_id = 'B']
T3[Tenant C data
tenant_id = 'C']
end
App --> DB
DB --> T1
DB --> T2
DB --> T3
style T1 fill:#ffcccc
style T2 fill:#ccffcc
style T3 fill:#ccccff
Characteristics
- All tenants share same app servers and database
- Data distinguished by
tenant_idcolumn - Every query must include:
WHERE tenant_id = 'X'
Pros
- Lowest cost: Maximum resource sharing
- Operational simplicity: One codebase, one database to manage
- Easiest onboarding: New tenant is just a row
Cons
- Security risk: One bug exposes all tenant data
- Noisy neighbor: One tenant's heavy load affects all
- Limited customization: All tenants get same schema
- Compliance challenges: Hard to isolate for regulations (GDPR, HIPAA)
Best For
- Startups optimizing for cost
- B2C SaaS with homogeneous tenants
- Low security/compliance requirements
2. Shared Database, Separate Schema
graph TB
subgraph "Application Layer"
App[Application Server
Multi-schema aware]
end
subgraph "Single Database Instance"
DB[(PostgreSQL)]
SchemaA[Schema: tenant_a
users, orders, products]
SchemaB[Schema: tenant_b
users, orders, products]
SchemaC[Schema: tenant_c
users, orders, products]
end
App --> |Connection pool A| SchemaA
App --> |Connection pool B| SchemaB
App --> |Connection pool C| SchemaC
DB --> SchemaA
DB --> SchemaB
DB --> SchemaC
style SchemaA fill:#ffcccc
style SchemaB fill:#ccffcc
style SchemaC fill:#ccccff
Characteristics
- Each tenant has its own database schema
- Queries:
SELECT * FROM tenant_a.users - Supports per-tenant schema customization
Pros
- Better isolation: Accidental cross-tenant query fails
- Customization: Each tenant can have custom columns
- Easier migration: Export one schema = one tenant
- Resource limits: Per-schema quotas (PostgreSQL)
Cons
- Schema sprawl: Thousands of schemas in one DB
- Migration complexity: Schema changes × N tenants
- Still noisy neighbor: Shared CPU, memory, disk
- Connection limits: Each schema needs connection pool
Best For
- B2B SaaS with moderate tenant count (10s-100s)
- Need for per-tenant customization
- Medium security requirements
3. Separate Database per Tenant
graph TB
subgraph "Application Layer"
App[Application Server
Database router]
end
subgraph "Shared Database Server"
Server[PostgreSQL Server]
DBA[(Database: tenant_a)]
DBB[(Database: tenant_b)]
DBC[(Database: tenant_c)]
end
App --> |Route based on tenant_id| Server
Server --> DBA
Server --> DBB
Server --> DBC
style DBA fill:#ffcccc
style DBB fill:#ccffcc
style DBC fill:#ccccff
Characteristics
- Each tenant has its own database
- Application routes requests to correct database
- Strong logical isolation
Pros
- Strong isolation: Query cannot access other tenant's data
- Independent backups: Restore one tenant without affecting others
- Performance isolation: Slow query affects only that tenant
- Easier compliance: Can encrypt/isolate specific tenants
Cons
- Database sprawl: Hundreds of databases to manage
- Higher cost: Each DB has overhead (connections, buffers)
- Complex migrations: Deploy schema change × N databases
- Still noisy neighbor: Shared server CPU, memory, disk
Best For
- Regulated industries (healthcare, finance)
- High-value enterprise customers
- Need strong isolation without full infrastructure separation
4. Separate Infrastructure (Silo Model)
graph TB
subgraph "Tenant A Infrastructure"
AppA[App Servers]
DBA[(Database)]
CacheA[Redis]
end
subgraph "Tenant B Infrastructure"
AppB[App Servers]
DBB[(Database)]
CacheB[Redis]
end
subgraph "Tenant C Infrastructure"
AppC[App Servers]
DBC[(Database)]
CacheC[Redis]
end
LB[Load Balancer
Tenant Router]
User[Users] --> LB
LB --> AppA
LB --> AppB
LB --> AppC
style AppA fill:#ffcccc
style AppB fill:#ccffcc
style AppC fill:#ccccff
Characteristics
- Each tenant gets dedicated app servers, database, cache
- Completely isolated infrastructure
- Can be on-prem or separate AWS accounts/VPCs
Pros
- Maximum isolation: No resource contention
- Custom versions: Each tenant can run different code version
- Compliance friendly: Easy to meet data residency requirements
- Performance guarantees: No noisy neighbor
Cons
- Highest cost: No resource sharing
- Operational nightmare: Manage N complete stacks
- Slow onboarding: Provisioning new tenant takes hours
- Difficult updates: Deploy to hundreds of environments
Best For
- Enterprise customers paying premium
- Strict compliance (government, healthcare)
- Need custom code per tenant
Isolation Model Comparison
| Aspect | Shared Everything | Separate Schema | Separate Database | Separate Infra |
|---|---|---|---|---|
| Cost | Lowest | Low-Medium | Medium-High | Highest |
| Security | Weak | Medium | Strong | Maximum |
| Noisy Neighbor | High risk | High risk | Medium risk | No risk |
| Customization | Limited | Per-schema | Per-database | Fully custom |
| Onboarding | Instant | Seconds | Minutes | Hours |
| Tenant Limit | Millions | Thousands | Hundreds | Tens |
| Operations | Simple | Moderate | Complex | Very complex |
Noisy Neighbor Problem
The noisy neighbor problem occurs when one tenant's resource consumption impacts other tenants' performance. It's the primary downside of resource sharing in multi-tenant systems.
Manifestations
1. CPU Contention
- Scenario: Tenant A runs expensive analytics query
- Impact: Tenant B's simple queries become slow
- Example: API latency spikes from 50ms → 5s
2. Memory Pressure
- Scenario: Tenant A caches 10GB of data
- Impact: Database starts swapping, all tenants slow down
- Example: Query latency increases due to disk I/O
3. Disk I/O Saturation
- Scenario: Tenant A imports 1TB of data
- Impact: Disk IOPS consumed, all queries wait
- Example: Dashboard takes 30s to load instead of 2s
4. Connection Exhaustion
- Scenario: Tenant A opens 500 database connections
- Impact: Connection pool exhausted, other tenants get errors
- Example: "Too many connections" error
5. Queue Backlog
- Scenario: Tenant A enqueues 1M background jobs
- Impact: Other tenants' jobs delayed for hours
- Example: Email not sent for 3 hours
Detection
graph TB
subgraph "Monitoring System"
M1[Per-Tenant Metrics
CPU, Memory, IOPS, Query time]
M2[Anomaly Detection
Spike detection]
M3[Correlation Analysis
When Tenant A heavy, Tenant B slow]
end
subgraph "Alerting"
A1[Alert: Tenant A using 80% CPU]
A2[Alert: p99 latency spike for Tenant B]
A3[Correlation: Tenant A heavy load = Tenant B degradation]
end
M1 --> M2
M2 --> A1
M2 --> A2
M1 --> M3
M3 --> A3
Mitigation Strategies
1. Resource Quotas (see next section)
Limit per-tenant CPU, memory, IOPS, connections.
2. Rate Limiting
- API rate limits: 1000 requests/min per tenant
- Query rate limits: 100 queries/sec per tenant
- Background job rate: 10 jobs/sec per tenant
3. Query Timeout
- Kill queries running longer than 30 seconds
- Prevents one tenant's runaway query blocking others
4. Priority Queues
- Enterprise tenants get high-priority queue
- Free tenants in low-priority queue
- Ensures premium customers unaffected
5. Tenant Sharding
- Separate heavy tenants onto dedicated shards
- Small tenants colocated, large tenants isolated
6. Circuit Breakers
- Temporarily block tenant exceeding quota
- Return "503 Service Unavailable" until cooldown
Real-World Example: AWS Lambda
Problem: Lambda is multi-tenant. How does AWS prevent noisy neighbors?
AWS Solutions
- Firecracker microVMs: Strong isolation between functions
- Concurrency limits: Max 1000 concurrent executions per account (default)
- Reserved concurrency: Guarantee capacity for critical functions
- Memory limits: Each function has max memory (128MB-10GB)
- Execution time limit: Max 15 minutes, prevents runaway functions
- Regional quotas: Limits per region, not global
Resource Quotas
Resource quotas limit per-tenant consumption of CPU, memory, disk, IOPS, connections, and other resources. They're essential for fair resource allocation and preventing noisy neighbors.
Types of Quotas
| Resource | Quota Type | Implementation | Example |
|---|---|---|---|
| API Requests | Rate limit | Token bucket, sliding window | 1000 req/min |
| Storage | Hard limit | Filesystem quotas, DB monitoring | 100 GB per tenant |
| Database IOPS | Soft limit | Throttle queries when exceeded | 1000 IOPS |
| Compute (CPU) | Fair share | Cgroups, Kubernetes limits | 20% of CPU |
| Memory | Hard limit | Cgroups, container limits | 4 GB RAM |
| Connections | Pool limit | Connection pool per tenant | 50 connections |
| Background Jobs | Queue quota | Job queue with per-tenant limits | 100 jobs/min |
Quota Enforcement Architecture
graph TB
Request[User Request
tenant_id = 'A']
subgraph "API Gateway"
Auth[Authentication
Extract tenant_id]
RL[Rate Limiter
Check quota]
Cache[Quota Cache
Redis]
end
subgraph "Application"
App[App Server]
Meter[Resource Metering
Track usage]
end
subgraph "Database"
DB[(Database)]
Limiter[Query Resource Governor
CPU/IOPS limits]
end
Request --> Auth
Auth --> RL
RL --> Cache
Cache --> |Over quota| Reject[Return 429 Too Many Requests]
Cache --> |Within quota| App
App --> Meter
Meter --> |Update usage| Cache
App --> DB
DB --> Limiter
Rate Limiting Algorithms
1. Token Bucket
- How: Bucket holds N tokens. Each request consumes 1 token. Refill at rate R/sec.
- Pros: Allows bursts (if bucket full). Smooth rate limiting.
- Cons: Need to track bucket state per tenant.
- Example: 1000 token bucket, refill 100/sec = allow burst of 1000, then steady 100/sec.
2. Leaky Bucket
- How: Requests enter queue. Process at fixed rate R/sec.
- Pros: Perfectly smooth output rate.
- Cons: Queue can grow large. Adds latency.
3. Fixed Window
- How: Count requests in time window (e.g., 00:00-00:59). Reset each window.
- Pros: Simple to implement.
- Cons: Burst at window boundary (1000 at 00:59, 1000 at 01:00 = 2000 in 1 sec).
4. Sliding Window
- How: Count requests in last N seconds (rolling window).
- Pros: No boundary burst issue. Fair.
- Cons: More complex (track timestamps).
Database Resource Quotas
PostgreSQL: Resource Quotas Extension
-- Limit CPU time per query
ALTER ROLE tenant_a SET statement_timeout = '30s';
-- Limit memory per query
ALTER ROLE tenant_a SET work_mem = '256MB';
-- Limit connections
ALTER ROLE tenant_a CONNECTION LIMIT 50;
MySQL: Resource Groups
-- Create resource group for tenant
CREATE RESOURCE GROUP tenant_a_group
TYPE = USER
VCPU = 2 -- Max 2 CPUs
THREAD_PRIORITY = 0;
MongoDB: WiredTiger Cache Limit
// Limit cache per connection (client-side)
mongod --wiredTigerCacheSizeGB 4
Soft vs Hard Limits
| Type | Behavior | Example | Use Case |
|---|---|---|---|
| Hard Limit | Immediate rejection when exceeded | API returns 429 after 1000 req/min | Prevent abuse, protect system |
| Soft Limit | Warning or throttling, but allow | Slow down queries after 80% quota | Encourage upgrade, avoid sudden failure |
| Burstable | Allow temporary burst above limit | 1000 req/min avg, burst to 2000 | Handle traffic spikes gracefully |
Quota Monitoring and Alerts
graph LR
Meter["Resource Metering Per-tenant metrics"]
W1[Warning: 80% quota]
W2[Critical: 95% quota]
W3[Limit: 100% quota]
A2[Throttle: Slow down requests]
A3[Block: Return 429 errors]
Meter --> W1
Meter --> W2
Meter --> W3
W1 --> A1
W2 --> A2
W3 --> A3
Data Isolation
Data isolation ensures tenants cannot access each other's data, even if there's a bug in application logic. It's the most critical security requirement in multi-tenant systems.
Isolation Techniques
1. Application-Level Filtering (Weakest)
- Method: Every query includes
WHERE tenant_id = ? - Risk: Developer forgets filter → data leak
- Mitigation: ORM-level enforcement, code review, audits
// Dangerous: Missing WHERE clause
SELECT * FROM orders; // Returns all tenants' orders!
// Safe: Explicit filter
SELECT * FROM orders WHERE tenant_id = 'tenant-123';
2. Row-Level Security (Better)
- Method: Database enforces isolation at row level
- Benefit: Impossible to forget filter (database blocks it)
- Support: PostgreSQL RLS, Oracle VPD, SQL Server RLS
-- PostgreSQL Row-Level Security
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id')::text);
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Set tenant context per session
SET app.tenant_id = 'tenant-123';
-- Query automatically filtered by RLS policy
SELECT * FROM orders; // Only returns tenant-123's orders
3. Schema/Database Separation (Strong)
- Method: Each tenant has separate schema or database
- Benefit: Physical isolation (cross-tenant query fails)
- Example:
SELECT * FROM tenant_a.ordersvstenant_b.orders
4. Encryption per Tenant (Strongest)
- Method: Encrypt each tenant's data with separate keys
- Benefit: Even with direct DB access, can't read other tenants
- Use: Highly regulated industries (HIPAA, GDPR)
Row-Level Security Deep Dive
graph TB
App[Application]
Conn[Database Connection
Set tenant_id = 'A']
subgraph "PostgreSQL with RLS"
RLS[Row-Level Security Policy
Filter: tenant_id = current_tenant]
Table[Orders Table
tenant_id, order_id, amount]
end
App --> |SET app.tenant_id = 'A'| Conn
Conn --> |SELECT * FROM orders| RLS
RLS --> |Automatic filter:
WHERE tenant_id = 'A'| Table
Table --> |Return only Tenant A rows| Conn
Encryption per Tenant
Architecture
- Key hierarchy: Master key → Tenant key → Data encryption key
- Master key: Stored in AWS KMS / Azure Key Vault
- Tenant keys: Derived from master, unique per tenant
- DEKs: Encrypt actual data, stored with data (encrypted by tenant key)
Workflow
- User uploads document for Tenant A
- Application retrieves Tenant A's key from KMS
- Encrypt document with Tenant A's key
- Store encrypted document + encrypted DEK
- Tenant B cannot decrypt (doesn't have Tenant A's key)
Benefits
- Compliance: Meets data sovereignty requirements
- Data breach protection: Stolen DB dump is encrypted
- Key rotation: Rotate keys per tenant independently
Downsides
- Performance: Encrypt/decrypt overhead (10-30% CPU)
- Complexity: Key management infrastructure
- Cost: KMS API calls add up at scale
Data Isolation Testing
1. Automated Tests
- Create Tenant A and Tenant B
- Insert data for both
- Query as Tenant A, assert only Tenant A data returned
- Try to bypass filter (SQL injection, parameter tampering)
2. Penetration Testing
- Attempt to access other tenant's data via API
- Test IDOR (Insecure Direct Object Reference) vulnerabilities
- Example: Change
tenant_idin JWT, try to access data
3. Audit Logging
- Log all cross-tenant data access attempts
- Alert on anomalies (Tenant A accessing Tenant B's data)
- Investigate and fix bugs immediately
WHERE tenant_id exposed data. Discuss PostgreSQL RLS as the gold standard for shared DB. For highly regulated industries, mention encryption per tenant with separate keys.
Tenant Routing
Tenant routing determines how requests are mapped to the correct tenant's data and resources. The routing strategy depends on the isolation model.
Routing Strategies
1. Tenant ID in Request Header
- Method: Client sends
X-Tenant-ID: acme-corpheader - Pros: Simple, works with any domain
- Cons: Client can spoof header (need authentication)
GET /api/orders HTTP/1.1
Host: api.example.com
Authorization: Bearer <jwt-token>
X-Tenant-ID: acme-corp
2. Subdomain-Based Routing
- Method: Each tenant has subdomain:
acme.example.com - Pros: Clear tenant identity, can route to different servers
- Cons: SSL cert management (wildcard or per-tenant certs)
// Tenant A
https://acme.example.com/api/orders
// Tenant B
https://beta.example.com/api/orders
3. Path-Based Routing
- Method: Tenant ID in URL path:
/api/acme-corp/orders - Pros: Easy to implement, single domain
- Cons: Ugly URLs, need to extract tenant from path
4. JWT Token (Embedded Tenant ID)
- Method: Tenant ID in JWT payload:
{ "tenant_id": "acme-corp" } - Pros: Secure (signed token), no need for separate header
- Cons: Must validate JWT on every request
Routing Architecture
graph TB
User[User Request
acme.example.com/api/orders]
subgraph "Edge Layer"
LB[Load Balancer
Extract subdomain]
Router[Tenant Router
Lookup shard]
end
subgraph "Routing Table"
RT[Routing DB
tenant → shard mapping]
end
subgraph "Application Shards"
Shard1[Shard 1
Small tenants]
Shard2[Shard 2
Medium tenants]
Shard3[Shard 3
Enterprise - Acme]
end
User --> LB
LB --> |tenant_id = 'acme'| Router
Router --> RT
RT --> |acme → Shard 3| Router
Router --> Shard3
Tenant Lookup and Caching
Problem: Performance
Looking up tenant shard/database for every request adds latency.
Solution: Multi-Level Caching
- L1 (Application memory): In-memory cache (5-min TTL)
- L2 (Redis): Shared cache across app servers (1-hour TTL)
- L3 (Database): Authoritative source
Lookup Flow
- Check L1 cache (in-memory): HIT → return in 0.01ms
- Check L2 cache (Redis): HIT → return in 1ms
- Check L3 (Database): MISS → return in 10ms, cache in L1 and L2
Cache Invalidation
- When tenant moves shards (rebalancing)
- Invalidate L1 and L2 caches immediately
- Use cache versioning or pub/sub for invalidation
Custom Domains
Requirement
Enterprise tenants want custom domains: app.acme.com instead of acme.example.com
Implementation
- DNS CNAME: Customer creates CNAME pointing to your load balancer
- Domain verification: Tenant adds TXT record to prove ownership
- SSL certificate: Auto-generate via Let's Encrypt or AWS ACM
- Routing: Map
app.acme.com→ tenant ID in routing table
Challenges
- SSL cert management: Generate, renew, revoke certs per domain
- Wildcard limits: Can't wildcard across tenants (need per-domain certs)
- DNS propagation: Custom domain changes take time (TTL)
Scaling Multi-Tenant Systems
Scaling multi-tenant systems requires balancing tenant distribution across infrastructure while maintaining isolation and performance.
Tenant Placement Strategies
1. Bin Packing (Cost-Optimized)
- Goal: Maximize tenants per server (minimize cost)
- Algorithm: Pack small tenants together, isolate large tenants
- Risk: Noisy neighbor if estimation is wrong
2. Consistent Hashing
- Goal: Distribute tenants evenly, minimize reassignment on scale
- Algorithm: Hash(tenant_id) % N shards
- Benefit: Add shard → only 1/N tenants move
3. Tiered Placement
- Tiers: Free (shared), Pro (shared), Enterprise (dedicated)
- Free tier: 1000 tenants per shard, no guarantees
- Pro tier: 100 tenants per shard, quota guarantees
- Enterprise tier: Dedicated infrastructure
4. Geographic Placement
- Goal: Data residency compliance (GDPR, data sovereignty)
- Strategy: EU tenants → EU region, US tenants → US region
- Trade-off: More infrastructure to manage per region
Tenant Sharding Architecture
graph TB
subgraph "Shard 1 - Small Tenants"
S1[1000 small tenants
avg 10 req/sec each]
DB1[(Database 1)]
end
subgraph "Shard 2 - Medium Tenants"
S2[100 medium tenants
avg 100 req/sec each]
DB2[(Database 2)]
end
subgraph "Shard 3 - Enterprise Tenant A"
S3[Tenant A
10,000 req/sec]
DB3[(Database 3
Dedicated)]
end
subgraph "Shard 4 - Enterprise Tenant B"
S4[Tenant B
5,000 req/sec]
DB4[(Database 4
Dedicated)]
end
Router[Tenant Router] --> S1
Router --> S2
Router --> S3
Router --> S4
Rebalancing Tenants
When to Rebalance
- Shard overloaded: CPU > 80%, move tenants to underutilized shard
- Tenant outgrows tier: Small tenant becomes large, move to dedicated shard
- New shards added: Redistribute for even load
Rebalancing Process
- Identify target tenant: Tenant to move from Shard A → Shard B
- Copy data: Replicate tenant's data to Shard B
- Dual-write phase: Write to both shards during transition
- Update routing: Point new requests to Shard B
- Drain old shard: Wait for in-flight requests to complete
- Delete old data: Remove tenant from Shard A
Zero-Downtime Migration
- Use feature flags to route traffic during migration
- Dual-write phase ensures no data loss
- Rollback capability if issues detected
Auto-Scaling Multi-Tenant Systems
| Component | Scaling Strategy | Trigger |
|---|---|---|
| Application Servers | Horizontal (add more servers) | CPU > 70% or request queue depth |
| Databases | Read replicas (scale reads) | Read IOPS > 80% |
| Cache (Redis) | Add cluster nodes | Memory > 80%, eviction rate high |
| Background Workers | Add worker instances | Queue depth > 1000 jobs |
| Shards | Add new shards, rebalance | All shards > 70% capacity |
Hot Tenant Problem
Problem
One tenant suddenly goes viral or runs a major campaign. Traffic spikes 100x.
Solutions
- Detect hot tenant: Monitor per-tenant request rate
- Migrate to dedicated shard: Move hot tenant off shared shard
- Scale tenant's resources: Add dedicated servers/DB for that tenant
- Rate limiting: Enforce quota to prevent overload
- CDN/caching: Cache hot tenant's content at edge
Example: Shopify Black Friday
- Major merchants see 50-100x traffic spike
- Shopify pre-provisions dedicated infrastructure for top merchants
- Auto-scaling adds capacity for medium merchants
- Small merchants stay on shared infrastructure with burst capacity
Cost Attribution and Billing
Multi-tenant systems must track resource usage per tenant for billing, cost analysis, and identifying optimization opportunities.
What to Meter
| Resource | Metric | Granularity | Billing Model |
|---|---|---|---|
| API Requests | Request count | Per request | $0.01 per 1K requests |
| Storage | GB-months | Daily snapshot | $0.10 per GB per month |
| Compute | CPU-hours | Per minute | $0.05 per vCPU-hour |
| Bandwidth | GB egress | Per request | $0.05 per GB |
| Database Queries | Query count or IOPS | Per query | $0.01 per 10K queries |
| Background Jobs | Job count or duration | Per job | $0.001 per job |
Metering Architecture
graph TB
subgraph "Application"
API[API Request]
Meter[Metering Agent
Capture: tenant_id, resource, quantity]
end
subgraph "Metering Pipeline"
Kafka[Kafka
Metering events stream]
Aggregate[Stream Processor
Aggregate per tenant per hour]
end
subgraph "Storage"
TSDB[(Time-Series DB
InfluxDB, Prometheus)]
Billing[(Billing DB
Usage records)]
end
subgraph "Billing System"
Invoice[Invoice Generator
Monthly bills]
Dashboard[Usage Dashboard
Tenant view]
end
API --> Meter
Meter --> Kafka
Kafka --> Aggregate
Aggregate --> TSDB
Aggregate --> Billing
Billing --> Invoice
TSDB --> Dashboard
Metering Best Practices
1. Asynchronous Metering
- Don't block requests waiting for metering
- Emit metering events to queue (Kafka, Kinesis)
- Process asynchronously in background
2. Idempotency
- Use unique event IDs to deduplicate
- Prevent double-counting if event replayed
- Critical for accurate billing
3. Aggregation Levels
- Real-time: Track current usage (rate limiting)
- Hourly: Aggregate for dashboards
- Daily: Aggregate for billing
- Monthly: Roll up for invoice
4. Data Retention
- Raw events: 30 days (for debugging disputes)
- Hourly aggregates: 1 year
- Daily aggregates: Forever (tax/audit requirements)
Cost Attribution Challenges
1. Shared Resources
Problem: Database CPU shared by multiple tenants. How to attribute?
Solutions:
- Track query execution time per tenant
- Estimate: (tenant query time) / (total query time) × (DB cost)
- Or: Use separate DB per tenant (easier attribution but higher cost)
2. Overhead Costs
Problem: Load balancers, monitoring, ops team aren't per-tenant.
Solutions:
- Allocate overhead proportionally to tenant usage
- Example: Tenant uses 10% of API requests → allocate 10% of load balancer cost
3. Free Tier / Trial Users
Problem: Free users consume resources but don't pay.
Solutions:
- Track cost per free user (visibility)
- Set aggressive quotas to limit free tier costs
- Monitor conversion rate (free → paid) to justify CAC
Real-World Example: AWS Cost Attribution
Multi-Tenant SaaS on AWS
- EC2 instances: Tag with tenant_id for dedicated instances, allocate shared instances by CPU usage
- RDS: Dedicated DB = direct cost, shared DB = allocate by query time
- S3 storage: Use bucket prefixes (
s3://bucket/tenant-123/) for per-tenant metering - Lambda: Tag invocations with tenant_id, AWS Cost Allocation Tags
- CloudWatch: Per-tenant metrics via custom dimensions
AWS Cost and Usage Report (CUR)
- Download detailed billing data
- Parse and attribute to tenants based on tags
- Build custom dashboards for cost visibility
Usage-Based Pricing Models
| Model | Description | Example |
|---|---|---|
| Tiered | Fixed tiers with usage limits | Free (1K req/mo), Pro ($50 for 100K), Enterprise (custom) |
| Pay-As-You-Go | Price per unit of usage | $0.01 per 1K API requests |
| Hybrid | Base fee + overage charges | $100/mo includes 50K requests, then $0.02 per 1K |
| Commitment | Prepay for capacity, discount | Reserve 1M requests/mo for $500 (vs $1000 pay-as-you-go) |
Key Takeaways
- Isolation models range from shared everything (lowest cost, weakest isolation) to separate infrastructure (highest cost, strongest isolation). Most systems use tiered placement.
- Noisy neighbor problem occurs when one tenant's resource usage impacts others. Mitigate with resource quotas, rate limiting, and tenant sharding.
- Resource quotas enforce fair sharing via API rate limits, CPU limits (cgroups), memory limits, and IOPS throttling. Use token bucket algorithm for rate limiting.
- Data isolation requires defense-in-depth: application-level filtering (weakest), row-level security (strong), separate schemas/databases (stronger), encryption per tenant (strongest).
- Tenant routing uses subdomains, headers, or JWT tokens to identify tenants. Cache routing lookups (L1 in-memory, L2 Redis) for performance.
- Scaling requires smart tenant placement (bin packing, tiered, geographic) and rebalancing with zero downtime (dual-write phase, gradual cutover).
- Cost attribution tracks per-tenant resource usage for billing. Meter asynchronously, ensure idempotency, and allocate shared costs proportionally.