Cell-Based Architecture: How to Use Cell Boundaries to Prevent a Single Tenant's Failure from Taking Down Your Entire SaaS
When operating a SaaS, there comes a day you suddenly receive a Slack notification saying "the entire service slowed down because of one specific customer." The noisy neighbor problem, a full outage caused by a bad deployment, a single availability zone network issue cascading to countless users — the outage that prompted Slack to migrate to a cellular architecture was exactly that kind of case.
Cell-Based Architecture is a structural answer to this problem. Rather than preventing failures from happening, it's a design that contains the blast radius of failures within cell boundaries. The SaaS meets cell-based architecture session at AWS re:Invent 2024 prompted me to revisit why this pattern is such a natural fit for multi-tenant SaaS. (The URL path mixes 2024/2025, but the session itself was presented at re:Invent 2024.)
In this article, I'll walk through exactly what a cell is and what components it consists of, how requests are actually routed to cells, and why Shuffle Sharding provides stronger failure isolation than simple sharding — with code and diagrams.
What Is a Cell — An Isolated Unit Containing a Complete Universe
Defining a Cell
A Cell is an independently deployable unit that self-contains every component needed to process a request from start to finish. Compute (service instances), database, cache, and message queue — everything lives inside a single cell.
In a traditional multi-tenant architecture, all tenants share the same database and services. If one tenant exhausts the DB connection pool, everyone is affected. Cell-based architecture fundamentally eliminates these shared points.
Tenant Isolation vs. Cell Isolation — Two Concepts Not to Confuse
This is the most common point of confusion when first learning cell-based architecture.
| Category | Tenant Isolation | Cell Isolation |
|---|---|---|
| Question | Who can read what? | How far does one tenant's failure spread? |
| Addresses | Data access rights, security boundaries | Failure propagation, performance noise |
| Implemented at | Application layer, IAM | Infrastructure, deployment boundaries |
| Purpose | Information security | Availability guarantee |
The two concepts are complementary but do not substitute for each other. Even with excellent cell isolation, the application layer can still incorrectly expose data between tenants; conversely, even with perfect tenant isolation, tenants within the same cell can still degrade each other's performance.
Why Now
It was a signal when the AWS Well-Architected Framework published "Reducing the Scope of Impact with Cell-Based Architecture" as an official whitepaper. This was once a pattern operated only by hyperscalers like AWS, Netflix, and Amazon, but as the Kubernetes ecosystem has matured sufficiently and platform engineering culture has spread, it is now accessible to smaller teams as well.
In June 2025, AWS added routing rules to API Gateway custom domains. The approach uses MatchHeaders and MatchBasePaths conditions to specify target APIs and stages — which can effectively function as a managed cell router. For infrastructure teams, the cost of building a cell router from scratch has dropped significantly.
Core Components and Routing Flow
Cell Router — Where Everything Begins
The cell router is an intelligent traffic director that examines incoming requests and decides which cell to send them to. Routing keys are typically tenant ID, user ID, or region information.
The following is a conceptual example. In actual production, a distributed store such as DynamoDB or Redis is used as the cell registry.
# Conceptual example — not an actual framework API
from dataclasses import dataclass
from typing import Literal, Optional
Health = Literal["healthy", "degraded", "unhealthy"]
@dataclass
class CellInfo:
cell_id: str
endpoint: str
health: Health
current_load: float # 0.0 to 1.0
class CellRouter:
def __init__(self, registry: "CellRegistry"):
self.registry = registry
def route(self, tenant_id: str) -> Optional[CellInfo]:
cell_id = self.registry.get_cell_for_tenant(tenant_id)
if not cell_id:
return None
cell = self.registry.get_cell_info(cell_id)
if cell.health == "unhealthy":
# In a Shuffle Sharding setup, select a healthy cell
# from the other cells assigned to this tenant
cell = self.registry.find_healthy_cell_for_tenant(tenant_id)
return cellfind_healthy_cell_for_tenant is a method that picks a healthy cell from the Shuffle Sharding assignment results (when a tenant spans multiple cells) covered later. With a single-cell assignment model, you would handle this with a circuit breaker or retry policy instead of a fallback.
Cell Registry — Global Metadata Store
The cell registry is a global store that tracks which tenant belongs to which cell and the current status of each cell. In AWS environments, DynamoDB is frequently used for this role, since low latency and high availability are essential.
# Conceptual example of using DynamoDB as a cell registry
from datetime import datetime, timezone
import boto3
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
table = dynamodb.Table('CellRegistry')
def get_cell_for_tenant(tenant_id: str) -> dict:
response = table.get_item(Key={'tenant_id': tenant_id})
return response.get('Item', {})
def register_tenant(tenant_id: str, cell_id: str, cell_endpoint: str):
table.put_item(Item={
'tenant_id': tenant_id,
'cell_id': cell_id,
'cell_endpoint': cell_endpoint,
'assigned_at': datetime.now(timezone.utc).isoformat(),
})Shuffle Sharding — Why It Provides Stronger Failure Isolation Than Simple Sharding
The Limits of Conventional Sharding
Conventional sharding assigns each tenant to a single fixed cell. With four cells, a tenant belongs to cell 1, cell 2, cell 3, or cell 4. If cell 1 fails, all tenants in cell 1 are directly affected. With N cells, the proportion impacted by any one failure is roughly 1/N.
The Core Idea of Shuffle Sharding
Shuffle Sharding assigns each tenant a unique combination of cells. For example, with 8 cells where each tenant uses 2 of them, Tenant A uses cells 1 and 3, Tenant B uses cells 2 and 5, and Tenant C uses cells 1 and 7.
What matters here is not so much "how much do tenants overlap in cells" but rather the proportion of tenants that become completely unavailable when a single cell goes down. In the example above, even if cell 1 fails, Tenant A and Tenant C each have their remaining cells (3 and 7) to route to, maintaining partial availability. Complete failure only occurs if all cells assigned to a tenant go down simultaneously — and since there are C(8,2)=28 possible combinations when picking 2 cells out of 8, the probability that any two tenants share the exact same combination is 1/28.
As you increase the number of cells N and the cells-per-tenant k, the number of combinations C(N,k) grows rapidly, so the number of tenants that fully share a failing cell group becomes very small. Where conventional sharding meant 1 cell down = 1/N of tenants fully down, Shuffle Sharding replaces that with partial degradation, and the number of tenants experiencing complete outage drops to a much smaller value. This is exactly why the AWS Route 53 infrastructure team adopted Shuffle Sharding from the beginning.
# Conceptual example of Shuffle Sharding tenant-to-cell assignment
import hashlib
from itertools import combinations
def assign_cells_to_tenant(
tenant_id: str,
all_cell_ids: list[str],
cells_per_tenant: int = 2,
) -> list[str]:
"""
Example of deterministically selecting a cell combination using tenant ID as a seed.
Note: this function only guarantees the same result when the order and set of
all_cell_ids never change. Adding or removing even one cell reshuffles all
combination indices, reassigning existing tenants. In production, run this
calculation only at initial provisioning time, store the result in the registry,
and prefer the registry value for all subsequent routing.
"""
hash_val = int(hashlib.sha256(tenant_id.encode()).hexdigest(), 16)
all_combos = list(combinations(all_cell_ids, cells_per_tenant))
selected = all_combos[hash_val % len(all_combos)]
return list(selected)
all_cells = ["cell-1", "cell-2", "cell-3", "cell-4",
"cell-5", "cell-6", "cell-7", "cell-8"]
tenant_a_cells = assign_cells_to_tenant("tenant-a", all_cells)
tenant_b_cells = assign_cells_to_tenant("tenant-b", all_cells)Real-World Cases — How Slack, DoorDash, and Netflix Did It
Slack: The Regional Outage That Drove the Migration
Slack described in its engineering blog that after experiencing an outage where a network issue in a single AWS availability zone propagated to the entire service, they decided to migrate to a Cellular Architecture. Since workspaces served as natural isolation boundaries, separating workspace units into independent failure domains aligned well with the domain model. (Some articles cite specific figures like "73 hours," but since these are not directly verifiable in the original source, this article focuses on the nature of the incident rather than specific numbers.)
DoorDash: Transitioning to Cell-Based Isolation
The DoorDash engineering team has also shared their journey of transitioning from a single large system to a structure of a small number of independent cells. The approach involves deploying each service to a Kubernetes cluster within a specific cell and combining zone-aware routing to contain traffic imbalances and blast radius within cell boundaries. (Internal project codenames were not used in this article as they could not be verified in official sources.)
Netflix: Regional and Functional Partitioning
Netflix partitions workloads by region and function, operating numerous cells. Each cell has its own video, recommendation, and telemetry services, with the structural goal of preventing infrastructure failures, traffic spikes, and application bugs from propagating globally.
A common observation across all three companies is that cell migration was chosen not as a preventive refactoring, but as a structural remedy following actual major outages. This is a useful signal when deciding when to adopt the pattern.
Trade-offs and Anti-Patterns
Summary of Pros and Cons
| Item | Details |
|---|---|
| Failure isolation | Failures are contained at the cell level, preventing full service outages |
| Noisy neighbor elimination | SLA guarantees per service tier |
| Progressive deployment | Canary deployments at the cell level, minimizing deployment risk |
| Independent scaling | Ability to scale specific cells vertically or horizontally |
| Compliance | Data residency requirements like GDPR can be met at the cell level |
| Operational complexity | Sophisticated automation is required to manage many cells consistently |
| Increased cost | DB, cache, and services are duplicated per cell; observability data grows proportionally with cell count |
| Cross-cell data handling | Full-tenant analytics and cross-cell reporting are the hardest technical problems |
Anti-Patterns Commonly Encountered in Practice
1. Sharing a Database Across Cells
The moment multiple cells point to the same DB to save on RDS costs, all isolation benefits disappear. If cost is a concern, the right move is to replace PostgreSQL with Aurora Serverless or revisit your cell sizing policy.
2. Synchronous Cross-Cell Calls
If a service in Cell A calls a service in Cell B synchronously, the failures of the two cells become coupled. If cross-cell communication is unavoidable, design it as asynchronous event-driven communication — and minimize it even then.
3. Deploying to All Cells Simultaneously
Acting on the misconception that "having multiple cells means deployments can be faster" and deploying to all cells at once defeats the entire purpose of the cell structure. For canary deployments to be meaningful, you must deploy to one cell first, observe, and then roll out sequentially.
4. Premature Adoption
For small teams, low traffic, and simple domains, this is clearly over-engineering. Approaching it with the motivation of "we need to do it like Netflix" only increases operational burden.
Determining Cell Size — The Most Frequently Asked Question
When tenants grow, whether to add a new cell or expand existing ones has no clear-cut answer. The criteria commonly considered are:
- Set a target number of tenants per cell in advance; provision a new cell when that number is reached
- Assign enterprise-tier customers to dedicated cells
- In AWS EKS-based architectures, separating each cell into a distinct AWS account is a referenced pattern for independently managing IAM, service limits, and billing
Tool Selection — Which Stacks Fit This Pattern
Infrastructure provisioning: Define cells as code with Terraform, Pulumi, or AWS CloudFormation. Without IaC, managing dozens of cells consistently is nearly impossible.
Container orchestration: Kubernetes / AWS EKS is the de facto standard. AWS officially provides guidance for EKS-based cell architectures.
Traffic routing: Amazon API Gateway (including the 2025 routing rules feature), Amazon Route 53 DNS-based routing, or Istio/Linkerd service mesh.
Cell registry: DynamoDB is frequently used for its low latency and global availability.
Observability: Configure distributed tracing across cells with OpenTelemetry, and visualize per-cell metrics with Prometheus + Grafana. Since observability data grows proportionally with cell count, cost planning should be done upfront.
Closing Thoughts
The essence of cell-based architecture is not eliminating failures, but containing them. Even the best-built systems will fail. What matters is a structure where that failure stays in cell 3 while tenants in cells 1 and 2 continue their service unaffected.
Applying this perspective to your current system makes the decision clear. Open the incident reports from the past six months and count how many cases involved a failure that started in "one tenant / one batch / one deployment" and dragged in other unrelated tenants. If you find three or more, your team has already reached the scale that needs cell boundaries — and those incidents are your hints for where to draw the first cell boundary. Conversely, if such cases are rare, the signal is that what you need right now is not a cell migration, but a cleanup of your tenant isolation layer and deployment pipeline.
Cells are not a trend you need to adopt someday — they are a tool you reach for when the incidents your organization faces demand it. The most reliable preparation is to start recording your incident history and per-tenant impact metrics now, so you can make that decision based on data when the time comes.
References
- AWS Well-Architected: Reducing the Scope of Impact with Cell-Based Architecture
- AWS re:Invent 2024 - SaaS meets cell-based architecture: A natural multi-tenant fit
- Guidance for a Cell-Based Architecture for Amazon EKS (AWS Official)
- GitHub - aws-solutions-library-samples/guidance-for-cell-based-architecture-on-aws
- Slack's Migration to a Cellular Architecture
- Workload Isolation Using Shuffle Sharding — Amazon Builders' Library
- AWS Architecture Blog: Containers and Cell-Based Design for Resiliency
- Cell-based architectures and Akka
- Cell-Based Architecture: Comprehensive Guide - DZone
- WSO2 Reference Architecture - Cell-Based