Ad Tech Multi-Tenancy: A Spectrum of Isolation Architectures on AWS
CK 3788 Created at Updated at
33700
Ad tech platforms serve hundreds to thousands of customers simultaneously: advertisers, agencies, publishers, networks. Each customer (tenant) expects isolated data, isolated compute resources, and isolated audit trails. Multi-tenancy is the architectural choice to share infrastructure (databases, servers, message queues) efficiently while maintaining logical and physical isolation. The challenge is preventing noisy neighbor problems—one tenant's expensive query or burst of traffic must not degrade another's performance—and enforcing fine-grained access control.
Three approaches span a spectrum: shared cluster with tenant tagging and row-level security (RLS) at the database layer, per-tenant logical schemas or databases within shared infrastructure, and per-tenant cells (complete physical isolation: dedicated cluster, database, message queues). The choice depends on customer base size, SLA strictness, and cost tolerance. A startup serving 100 agencies uses shared cluster with RLS; a mature platform with 10,000 SMB advertisers might use per-schema isolation; a mega-platform with demanding enterprise customers operates per-tenant cells.
This case explores isolation strategies, noisy-neighbor mitigation, tenant-aware billing/metering, and audit isolation using AWS services (Aurora PostgreSQL, DynamoDB, ECS Fargate, ElastiCache, AWS Cloud Map, IAM).
Key Requirements
Functional Requirements
Logical isolation: Tenant A cannot see Tenant B's data (campaigns, conversions, reports) via any query or API.
Rate limiting: Tenant A's workload (QPS, concurrent connections, storage growth) does not impact Tenant B's SLO.
Multi-tenancy abstraction: Queries automatically filtered by tenant_id; developers should not need to remember "WHERE tenant_id = ?" in every query.
All tenants share one Aurora PostgreSQL cluster, one ElastiCache Redis cluster, and one ECS Fargate service pool. Each row in a multi-tenant table includes a tenant_id column. Aurora Row-Level Security (RLS) enforces tenant filtering at the database layer: role-based policies automatically filter queries. Application-level middleware (running on ECS Fargate) injects tenant_id into request context and JWT claims. Rate limiting (Redis-backed, per tenant) enforces quota on API calls, concurrent connections, and query resource consumption.
Tenant A's expensive query: implement query timeout (e.g., 30 seconds) at application layer; kill runaway queries via Lambda. Per-tenant query budget (CPU cost units/hour) tracked in CloudWatch.
Connection pool exhaustion: Tenant A opens 50 connections, leaving limited pool for others. Mitigate: enforce max_connections per tenant in application (e.g., Tenant A max 25, others 5 each) via PgBouncer on ECS.
RLS bypass: developer forgets to apply RLS policy to new table. Mitigate: policy-as-code in Terraform; default-deny RLS on all tables; deny SELECT by default, require explicit grant.
Storage disk full: Tenant A's audit log grows unbounded. Mitigate: enforce storage quota per tenant (10 GB default) via S3 archival Lambda trigger; alert at 80%, block writes at 100%.
SLA tolerant of occasional latency spikes (p99 <500 ms acceptable).
Operations team small; cannot manage per-tenant infrastructure.
Cost per tenant must be minimal (startup mode).
Option 2: Per-Tenant Schemas or Databases (Logical Isolation)
Shared Aurora PostgreSQL cluster; each tenant gets dedicated schema (or separate database). Data is logically isolated at the schema level. Tenant_id no longer needed in every table; isolation enforced by schema-level permissions. Rate limiting still shared cluster-wide via ElastiCache, but each tenant's queries run in separate schema with dedicated indexes and statistics. Aurora supports up to 1000+ schemas per cluster.
Pros:
Query plan isolation: Tenant A's queries do not interfere with planner for Tenant B; each schema has independent statistics.
Cleaner access control: Postgres roles per tenant; GRANT only on schema_42 to tenant_42_role.
Easier debugging: tenant A's tables, indexes isolated; monitoring more transparent.
Index customization: Tenant A can add custom indexes without affecting others.
Data migration: moving tenant to different infrastructure simpler (dump schema, restore elsewhere).
Connection contention: shared pool; still vulnerable to noisy neighbor stealing connections.
Maintenance complexity: schema migration (e.g., adding column) requires batching across tenants; cannot be instant.
Shared query cache: Aurora query cache (prepared statements) shared; one tenant's queries can evict another's from cache.
Still single database failure: outage affects all tenants; no isolation at cluster level.
Failure Modes & Mitigations:
Tenant A maxes storage quota: Aurora disk fills, all tenants blocked. Mitigate: per-schema tablespace (separate EBS volume), enforce quota at tablespace level, alert before hit.
Tenant A's DDL locks schema: ALTER TABLE on Tenant A's campaigns blocks schema operations. Mitigate: use CONCURRENTLY option; schedule DDL during low-traffic windows; use EventBridge for coordinated maintenance.
Connection pool starvation: Tenant A opens 30 connections; others starve. Mitigate: PgBouncer connection pooling on ECS, per-tenant connection limits.
When to Choose:
Tenant count 500–5,000 with moderate isolation requirements.
Can manage per-schema backups, migrations.
Operations team 2–5 people; can automate schema provisioning via Terraform.
Need query plan isolation but not full physical isolation.
Each tenant (or cohort of <10 top tenants) gets dedicated infrastructure: Aurora PostgreSQL instance, ElastiCache Redis cluster, ECS Fargate service, Kinesis Data Streams topic. Tenants in different cells never share compute or storage. Only metadata (AWS Cloud Map service registry), billing systems (DynamoDB), and control plane (Route 53) are shared. This is the most operationally complex but provides complete isolation.
Pros:
Perfect isolation: Tenant A's hardware failure, DoS, or runaway query zero impact on Tenant B.
Custom SLA: each cell can have different Aurora backup frequency, failover policy, Kinesis shard count.
Scaling knob: Tenant A's growth does not affect capacity planning for others.
Debugging clarity: all metrics, logs, queries isolated; easy to correlate events to tenant.
Compliance: Tenant A in EU (eu-west-1), Tenant B in US (us-east-1); deploy cells in correct region; KMS keys per tenant.
Data residency: each tenant's data stays in specified region; satisfies regulatory requirements.
Data replication lag across cells: batch reporting queries hit stale data. Mitigate: eventual consistency acceptance; separate read-only Aurora replica in reporting region with hourly sync.
Cost explosion for small tenants: 1000 micro-tenants × $5,000/mo per cell = $5M/mo unviable. Mitigate: multi-tenant shared cluster for low-tier; cells only for enterprise.
When to Choose:
Tenant count <100 but each is enterprise (high SLA, compliance, custom integration).
Geographic distribution required (data residency, latency).
Tenant isolation for competitive or security reasons critical.
Operations team >10 people; infrastructure-as-code maturity high.
Comparison Table
Dimension
Shared + RLS
Per-Schema
Per-Cell
Operational Overhead
Low
Medium
High
Cost per Tenant
$100–500/mo
$500–2,000/mo
$5,000+/mo
Isolation
Logical
Logical
Physical
Noisy Neighbor Risk
High
Medium
None
Tenant Density
10,000+
1,000–5,000
<100
Query Plan Interference
Yes
No
No
Scale-Out (add tenant)
Instant
1–5 minutes (Terraform)
15–30 minutes (CloudFormation)
Debugging Ease
Hard (mixed CloudWatch logs)
Medium
Easy
Compliance/Audit
Moderate (RLS policies)
Good (schema-level)
Excellent (isolated KMS keys, logs)
Multi-Tenant Scaling
Yes (vertical limit ~5,000)
Yes (~2,000 limit)
No (cells independent)
Recommendation
Hybrid tiering strategy using AWS services:
Tier 1 (Free / Micro, <$100/mo): Shared Cluster with RLS
Single Aurora PostgreSQL cluster (shared across all Tier 1 tenants).
Tenant_id + RLS + ElastiCache rate limiting.
Storage quota: 1 GB, API quota: 10k calls/day.
SLA: best-effort, no uptime guarantee.
Scaling limit: ~5,000 tenants before p99 latency unacceptable.
Monitoring: CloudWatch metrics per tenant_id; SNS alerts on quota breaches.
Tier 2 (Pro / Standard, $100–2,000/mo): Per-Schema or Per-Database
Dedicated schema within shared Aurora cluster (or separate Aurora instance per 10 tenants).
Storage quota: 100 GB, API quota: 1M calls/day.
SLA: 99.5% uptime commitment.
Scaling limit: ~2,000 tenants on single cluster; beyond, shard into multiple clusters (geographic or cohort-based) via Route 53.
Monitoring: dedicated CloudWatch dashboards per schema; RDS Performance Insights for query optimization.