Cell-Based Architecture: Bulkheads and Blast-Radius Containment (2026)
Most outages are not caused by the failure of a single component. They are caused by the failure of a single component in a system where everything is connected to everything else. A bad deployment, a poisoned request, a noisy tenant or a half-dead network link starts small and then spreads along every shared dependency until the whole product is down. You can make each component more reliable, or you can change the geometry so that failures cannot travel far.
Cell-based architecture takes the second path. You run several complete, independent copies of a workload, called cells, put a deliberately dumb router in front, and pin each customer to one cell. When something breaks, it breaks inside one cell and the rest of your customers never notice. This guide explains the pattern from first principles, shows the routing and shuffle-sharding math with runnable code, and covers cell sizing, deployment, migration and the ways cells go wrong in practice.
What this covers: the bulkhead idea that cells generalise, the reference design (router, cells, control plane), shuffle sharding with worked combinatorics, a runnable cell router and placement simulator, deployment and observability patterns, cost and sizing trade-offs, and an honest list of failure modes.
Context and Background
The word “bulkhead” comes from ship design: watertight compartments mean a hull breach floods one compartment rather than the whole vessel. Michael Nygard popularised the software version in Release It!: partition resources such as thread pools, connection pools and queues so that one failing dependency cannot exhaust capacity shared by the rest. Libraries like Resilience4j and Polly implement it at the call level. That is the bulkhead pattern in its classic form, and it is valuable, but it protects a single process from a single bad dependency.
Cell-based architecture applies the same idea at the scale of an entire workload. AWS’s Well-Architected guidance defines it plainly: a cell-based architecture uses multiple isolated instances of a workload, where each instance is a cell; each cell is independent, does not share state with other cells, and handles a subset of the overall requests (AWS Well-Architected: Reducing the Scope of Impact with Cell-Based Architecture). The same guidance gives the headline arithmetic: with 10 cells serving 100 requests, a failure in one cell leaves 90 percent of requests unaffected.
The pattern has a long production record. Amazon Web Services uses cells inside services such as Route 53 and describes shuffle sharding, a close cousin, in its Builders’ Library. Slack, after a June 2021 incident in which a failing network link in one availability zone produced a gray failure, spent roughly a year and a half migrating to a cellular design where each availability zone is a cell that can be drained on demand (Slack Engineering: Slack’s Migration to a Cellular Architecture). Different companies, same conclusion: past a certain scale, the dominant risk is not a single server dying but a correlated failure that takes everyone down together.
It helps to separate three terms that are often confused. Blast radius is the fraction of users or requests affected by a given failure. Fault isolation boundary is the line that failures cannot cross, such as a process, a cell, an availability zone or a region. Scope of impact is the same idea measured in business terms. A cell is simply a fault isolation boundary that you design on purpose, around your own workload, rather than inheriting from the cloud provider.
Cells also relate closely to the topology questions we covered in our guide to multi-region active-active database architecture. Multi-region designs protect you from the loss of a region. Cells protect you from the failure modes that live inside a region and, importantly, from your own bad deploys, which no amount of regional redundancy can address because the bad code is replicated everywhere with the rest of your fleet.
One more piece of context: this is not microservices. Microservices split a system by function, so that the billing service and the search service fail independently. Cells split a system by customer, so that customer group A and customer group B fail independently, and each cell contains all the functions. The two compose well, but a microservices system with one shared deployment per service has a blast radius of 100 percent for any change to that service. Cells attack a different axis, and the axes are orthogonal.
The Reference Architecture: Router, Cells, Control Plane
Short answer: what is a cell-based architecture? It is a workload deployed as N identical, fully independent copies (cells), each serving a fixed subset of customers, fronted by a thin stateless router that maps a partition key to a cell. A separate control plane provisions, places and migrates customers. Failures and bad deployments stay inside one cell, so blast radius is roughly 1/N.

Figure 1: Reference design for cell-based architecture. Clients hit a single endpoint, a thin router resolves the partition key to a cell, and each cell is a complete stack with its own data store. The control plane manages placement and never sits in the request path.
The diagram shows the three roles. Understanding why each is kept separate is the main design skill; most cell implementations that disappoint do so because one of these boundaries leaked.
The cell: a complete workload with no shared state
A cell contains everything needed to serve a request end to end: load balancer or ingress, compute, caches, queues and, crucially, its own data store. The test is simple. If you can delete every other cell and this one keeps serving its customers correctly, it is a cell. If it needs a shared database, a shared cache cluster or a synchronous call to a global service to answer an ordinary request, it is not isolated, and the blast radius quietly returns to 100 percent through that dependency.
Cells are identical by construction. The same artifact, the same configuration schema and the same runbooks apply to every cell. That uniformity is what makes the pattern operable: you are running N copies of one thing, not N different things. It also means a cell can be sized, tested and load-tested once. Nothing about cell number seven should surprise you if you have understood cell number one.
Cells do not have to be a pile of separate accounts, though many teams use an account or a Kubernetes cluster per cell for the strongest isolation. A cell can be a namespace plus node pool plus database instance. AWS makes the point that building cells does not mean doubling or tripling your infrastructure: an application with 30 hosts can have the same 30 hosts, grouped into cells behind a router. The cost lives in the headroom and the duplicated fixed components, which we quantify later.
The cell router: the thinnest possible layer
The router has exactly one job: take a request, extract the partition key, look up the owning cell and forward the request. AWS describes it as the thinnest possible layer, responsible for routing to the right cell and only that. Everything else belongs elsewhere. The router must not authenticate users in a complex way, transform payloads, aggregate across cells or hold business logic, because every responsibility you add turns it into the one component whose failure is a 100 percent outage.
The partition key must align with the grain of the service. Customer or tenant ID is the common choice because it keeps all of a tenant’s data and traffic together. For a messaging product it might be the workspace; for an IoT platform it might be the site or fleet ID; for a payments platform, the merchant. Choose a key that is present on nearly every request and that does not require a lookup to extract. A key buried in a request body forces the router to parse payloads, which makes it fatter and more fragile.
Routing itself can be implemented three ways. A mapping table stores partition key to cell explicitly (a DynamoDB table, an etcd keyspace, a cached config) and gives you total control over placement and migration. A hash ring or modulo derives the cell from the key with no stored state, which is simple but makes rebalancing painful. A hybrid hashes into many logical buckets and maps buckets to cells, so you can move a bucket without touching the hash function. The hybrid is usually the right answer, and it is what the sample code below implements.
The router has to survive without its control plane. Cache the mapping locally, refresh it asynchronously, and keep serving the last known good table if the source is unreachable. A router that calls a database on every request has simply moved the shared dependency one hop earlier.
The control plane: placement, provisioning and migration
The control plane handles administration: creating cells, retiring them, assigning new customers and moving existing customers between cells. It is a separate system, and it is deliberately off the hot path. If the control plane is down, existing customers continue to be served; only changes are blocked. That asymmetry mirrors the classic control plane and data plane split used throughout AWS, and it is what keeps the pattern from adding a new single point of failure.
Placement policy lives here. A simple policy is “assign new customers to the cell with the most remaining headroom.” Better policies account for customer size, since one whale and a thousand small tenants have very different load profiles, and for compliance constraints such as data residency, which dictate that a tenant must land in a cell in a specific region. We cover those later in the placement section.

Figure 2: The request path in a cell-based architecture. The router extracts the partition key, resolves the cell from a locally cached table and forwards the request. The control plane updates that table out of band.
Why 1/N is the number, and why it is a floor
With N equal cells and uniformly distributed customers, a failure that is fully contained to one cell affects 1/N of traffic. Ten cells give 10 percent, a hundred cells give 1 percent. This is the principal selling point and it is real, but treat 1/N as the best case. Three things worsen it. Cells are rarely equal, so the cell holding your largest tenant carries far more than 1/N of revenue. Shared components, such as the router or a global identity service, are outside the cell boundary and fail for everyone. And deployments that touch all cells at once, which we return to, defeat the isolation entirely.
The better framing is that cells convert an unbounded risk into a bounded one. Without cells, the worst case for any bug is total failure. With cells, the worst case is the size of one cell, and you choose that size. The rest of this article is about making that bound real.
Shuffle Sharding: Making the Blast Radius Smaller Than One Cell
Plain cells give each customer a fixed home. Everyone in cell 3 shares fate. That is fine for infrastructure failures, but it is a poor fit for the other common failure: a single misbehaving customer whose traffic pattern, such as a poison request, a retry storm or an expensive query, takes down the cell it lives in and every neighbour with it. Shuffle sharding attacks exactly this case.
Short answer: what is shuffle sharding? Instead of assigning each customer to one fixed shard, assign each customer a small random subset of the available workers. Two customers rarely share the same subset, so a problem customer can only fully take out workers that few other customers are also using. With 8 workers and subsets of 2 there are 28 combinations, so impact drops to 1/28.
The Amazon Builders’ Library article that introduced the technique uses a clean example (Workload isolation using shuffle-sharding). Take a service of eight workers. Conventional sharding splits it into four shards of two workers, so one bad customer disables a shard and 25 percent of customers. With shuffle sharding, each customer gets a virtual shard of two workers chosen from eight. There are 28 unique pairs, so a single bad customer degrades one pair, and only the customers who happen to hold that exact pair lose both workers. The article calls that seven times better than regular sharding, and notes that any other customer shares at most one worker with the affected one.
The combinatorics, worked through
The count of distinct virtual shards is the binomial coefficient C(n, k): workers n, shard size k. The probability that two customers drawn at random hold the identical shard is 1/C(n, k). More useful for operations is the overlap distribution, which follows the hypergeometric distribution. For a pool of 100 workers with each customer assigned 5, I computed the following with the Python snippet below.
| Overlap with a bad customer | Probability for a random other customer |
|---|---|
| 0 workers shared | 76.96% |
| 1 worker shared | 21.14% |
| 2 workers shared | 1.84% |
| 3 workers shared | 0.059% |
| 4 workers shared | 0.00063% |
| All 5 shared (total loss) | 0.0000013% (one in 75.3 million) |
The key reading is the last row. The bad customer can knock out all five of its own workers, but a given neighbour loses everything only if it holds the identical five, which is a one in 75,287,520 event. Most neighbours share nothing, and the roughly 21 percent that share one worker still have four healthy workers. Provided clients retry against the other workers in their shard, which the Builders’ Library article flags as a requirement, those neighbours see a brief capacity dip rather than an outage.
Route 53 shows the idea at a scale where the numbers become striking. Its capacity is arranged into 2,048 virtual name servers and each hosted zone receives a shuffle shard of four. AWS reports that this yields about 730 billion possible shards, and that no two customer domains share more than two virtual name servers. You can verify the figure yourself: C(2048, 4) is 730,862,190,080. For a random pair of domains, the chance of sharing even one server is under 0.8 percent, and the chance of sharing all four is about one in 7.3 * 10^11.
Cells versus shuffle shards: complementary, not competing
Cells give you a hard, simple, human-explainable boundary: this cell, these customers, this deployment. Shuffle sharding gives you a statistical boundary that is much finer but harder to reason about operationally. The two stack. A common design is cells for coarse isolation and for deployment safety, with shuffle sharding inside a cell across its worker pool or across its partitions, so that one hostile tenant does not take a whole cell with it.
| Property | Plain sharding | Cells | Shuffle sharding |
|---|---|---|---|
| Isolation unit | Partition of data or compute | Complete workload instance | Random subset of workers |
| Blast radius | 1/shards | 1/cells | about 1/C(n,k) for the same bad actor |
| Deployment unit | Often shared | Yes, per cell | No, a routing concept |
| Operability | Simple | Simple, explicit | Harder, needs tooling |
| Protects against bad deploy | No | Yes, if staged | No |
| Protects against noisy tenant | Partly | Partly | Strongly |

Figure 3: Shuffle sharding versus fixed sharding. In fixed sharding, tenants A and B share fate completely. With shuffle shards each tenant holds a different pair of workers, so a failure triggered by one tenant leaves the other with at least one healthy worker.
Note the row on bad deploys. Shuffle sharding does nothing for a faulty release, because every worker runs the same code. Only cells with staged rollout contain that failure. This is the argument for using both rather than choosing one.
Walk-through: A Runnable Cell Router and Shuffle-Shard Simulator
The best way to internalise these ideas is to build them. The code below is deliberately small and has no dependencies beyond the Python standard library. It implements the hybrid routing scheme described earlier (hash into many buckets, map buckets to cells), a deterministic shuffle-shard assignment and a failure simulation that measures blast radius empirically.
Bucketed routing with a mapping table
The router hashes the partition key to one of 4,096 logical buckets and looks up the owning cell in a table. Moving a customer, or a group of customers, means re-pointing a bucket, not changing the hash. The table is a plain dictionary here; in production it would be a cached, versioned config.
import hashlib
from dataclasses import dataclass, field
BUCKETS = 4096
def bucket_of(partition_key: str) -> int:
digest = hashlib.sha256(partition_key.encode()).digest()
return int.from_bytes(digest[:4], "big") % BUCKETS
@dataclass
class CellRouter:
# bucket -> cell id; refreshed asynchronously, last known good wins
table: dict = field(default_factory=dict)
overrides: dict = field(default_factory=dict) # key -> cell, for pinned tenants
def cell_for(self, partition_key: str) -> str:
if partition_key in self.overrides:
return self.overrides[partition_key]
return self.table[bucket_of(partition_key)]
def build_even_table(cells: list[str]) -> dict:
return {b: cells[b % len(cells)] for b in range(BUCKETS)}
router = CellRouter(table=build_even_table([f"cell-{i}" for i in range(10)]))
router.overrides["tenant-whale-001"] = "cell-whale" # isolate a very large tenant
print(router.cell_for("tenant-42"), router.cell_for("tenant-whale-001"))
Two design points are baked in. The override map handles the whale problem, giving a very large or very sensitive tenant a dedicated cell. And the hash uses a stable cryptographic digest rather than Python’s built-in hash, which is randomised per process; a router whose fleet disagrees about placement would be disastrous.
Deterministic shuffle sharding
Shuffle-shard assignment must be a pure function of the customer ID, so that every router and every client computes the same subset with no coordination. Seeding a pseudo-random generator from the ID achieves that.
import random
from math import comb
def shuffle_shard(customer_id: str, workers: list[str], k: int) -> tuple:
rng = random.Random(hashlib.sha256(customer_id.encode()).digest())
return tuple(sorted(rng.sample(workers, k)))
workers = [f"w{i:03d}" for i in range(100)]
print(shuffle_shard("customer-a", workers, 5))
print(shuffle_shard("customer-b", workers, 5))
print("distinct shards:", comb(len(workers), 5)) # 75,287,520
Measuring blast radius by simulation
Now the payoff. We place 10,000 customers, choose one at random as the “poison” customer that kills every worker in its shard, and measure how many other customers lose all of their workers (total outage) or some of them (degraded). Compare fixed sharding with shuffle sharding at the same worker count.
import random, hashlib
from collections import Counter
def fixed_shard(customer_id, workers, k):
# contiguous groups of k workers: classic sharding
h = int.from_bytes(hashlib.sha256(customer_id.encode()).digest()[:4], "big")
groups = len(workers) // k
g = h % groups
return tuple(workers[g * k:(g + 1) * k])
def simulate(assign, customers, workers, k, trials=200, seed=7):
rng = random.Random(seed)
out_total = out_partial = 0
for _ in range(trials):
bad = rng.choice(customers)
dead = set(assign(bad, workers, k))
for c in customers:
if c == bad:
continue
lost = len(dead & set(assign(c, workers, k)))
if lost == k:
out_total += 1
elif lost > 0:
out_partial += 1
denom = trials * (len(customers) - 1)
return out_total / denom, out_partial / denom
customers = [f"c{i}" for i in range(2000)]
workers = [f"w{i:03d}" for i in range(100)]
for name, fn in [("fixed", fixed_shard), ("shuffle", shuffle_shard)]:
total, partial = simulate(fn, customers, workers, 5, trials=60)
print(f"{name:8s} total outage {total:.4%} degraded {partial:.4%}")
Expected behaviour, which follows from the math rather than from any benchmark: fixed sharding with 100 workers in groups of 5 gives 20 shards, so about 5 percent of neighbours suffer a total outage whenever one customer goes bad. Shuffle sharding reduces the total-outage fraction to effectively zero in a 2,000-customer simulation, while the degraded fraction rises to roughly 23 percent, the sum of the partial-overlap rows in the table above. This is the characteristic trade: shuffle sharding trades a small chance of complete failure for a larger chance of mild degradation, which is almost always the correct trade for a system whose clients can retry. Run the code yourself; exact simulated values will vary slightly with the seed.
Where the code stops being realistic
This simulator assumes the failure is exactly “the bad customer kills its whole shard”. Real poison traffic often degrades capacity gradually, and clients that retry aggressively onto healthy workers can spread the damage, which is the retry-storm failure covered in the next section. Treat the output as a model of structure, not a prediction of your incident rate. Pair it with distributed rate limiting at the cell edge, because limiting each tenant’s consumption inside its shard is what stops one tenant from ever exhausting the shard in the first place.
Placement strategy and cell sizing
Placement decides who shares fate with whom. Three policies are common. Hash placement is stateless and uniform but blind to customer size. Capacity-aware placement assigns each new tenant to the cell with the most headroom, which needs a control plane but balances load. Affinity placement co-locates tenants that interact, for example a parent organisation and its subsidiaries, to avoid cross-cell calls; it trades isolation for fewer cross-cell dependencies.
Cell size is a judgment call with a real tension. Small cells shrink blast radius and are cheap to test at full scale, but each carries fixed overhead (control-plane components, observability, minimum redundancy) and a higher chance that a single large tenant will not fit. Large cells are cost-efficient but make each failure more expensive. AWS’s public guidance does not prescribe a number, and neither should you copy one. A workable heuristic: pick the largest cell you can fully load-test in a pre-production environment, so that “this cell tolerates its maximum” is a tested statement rather than an assumption, and size so that losing one cell is an incident you can survive financially and contractually.
Keep headroom explicit. If you run ten cells and want to absorb the loss of one without degradation, each cell needs about 11 percent spare capacity, since the surviving nine must carry 10/9 of the load. That is the classic N+1 arithmetic applied to cells, and it is one place where cells have a real cost versus a single pooled fleet that shares spare capacity globally.
Deploying and Operating Cells: Where the Isolation Is Won or Lost
A cell architecture that deploys to every cell at once has the blast radius of a monolith with extra steps. The largest benefit of cells, and the one that surprises teams most, is not hardware fault tolerance but deployment safety. Most production incidents follow a change. If a change reaches one cell first and is observed before it reaches the next, a bad release harms 1/N of customers instead of all of them.
Staged rollout across cells
The standard approach is a wave deployment. Start with a canary cell that contains internal or low-risk tenants, bake for a fixed period while automated checks watch error rate, latency and saturation, then proceed through progressively larger groups of cells. AWS describes deploying “compartment by compartment”, meaning isolation boundary by isolation boundary, and the same principle applies to configuration, feature flags and schema changes. Treat configuration as code that is rolled out in waves too; many of the worst public outages were configuration pushes that were global by default.
Automated rollback matters more than the wave plan. Each wave should have a pass or fail gate based on cell-local metrics, and a failure should halt the pipeline and revert the affected cell without a human deciding. Bake time should be long enough to catch slow failures such as memory leaks and cache warm-up regressions, which a five-minute check will never see. If you want to validate that the gates actually trip, use fault injection; our walk-through of chaos engineering on Kubernetes with Chaos Mesh and Litmus shows how to define a steady state and break it deliberately in one cell.

Figure 4: Staged deployment through cells. A canary cell receives the change first; a health gate decides whether to proceed to the next wave or roll back, so a bad release is bounded to the cells already updated.
Draining a cell
A cell must be removable from rotation quickly, with a single operation that needs no root-cause understanding. This is the “big red button” Slack built. In its design, traffic could be moved away from an availability-zone cell in a goal of five minutes, with the control plane propagating changes in seconds and in-flight requests finishing gracefully. Slack drained by setting the weight of an AZ to zero using Envoy weighted clusters and runtime configuration over xDS, and could shift traffic in 1 percent increments, which let operators undrain slowly and test recovery with a sliver of traffic.
That granularity is worth copying. Ramping a recovering cell back from 1 percent to 5 to 25 percent catches the cold-cache and connection-storm failures that come from slamming a freshly restored cell with its full load. Draining is also your best mitigation tool during an incident: you do not need to know why cell 4 is unhealthy to stop sending it traffic, and that decouples mitigation from diagnosis, which is the single most useful property in the first ten minutes of an outage.
Observability must be cell-scoped
If your dashboards aggregate across all cells, a failure in one cell is diluted. With ten cells, a cell at 100 percent errors shows as a 10 percent global error rate, which many alert thresholds will ignore. Every metric, log line and trace needs a cell dimension, and alerting should evaluate per cell. Emit a canary request per cell from outside, since a cell that has silently lost its route will not report its own death. Compare each cell against its peers: because cells are identical, a cell that deviates from the fleet median is suspicious by definition, which gives you a cheap anomaly detector.
Keep the monitoring system itself outside the failure domain, or deployed per cell with a global rollup that tolerates cell absence. A health check that lives inside the cell it watches cannot tell you the cell is gone.
Migration between cells
Customers outgrow cells, cells need rebalancing, and compliance changes sometimes force a move. Migration is the hardest operation in the pattern because it temporarily violates “one customer, one cell.” The safe sequence has four steps. Copy the tenant’s data to the target cell while the source remains authoritative. Catch up via replication or change-data-capture until the lag is near zero. Briefly block writes for that tenant, flip the router entry, and unblock. Verify, then retire the source copy after a cooling-off period. The write freeze should last seconds, and it affects only the migrating tenant. Because the router mapping is the commit point, a failed migration is rolled back by not flipping it.
Test migration before you need it. A cell architecture whose only migration path has never been exercised is a trap: the first time you need to evacuate a hot cell is a bad time to discover the tooling has bit-rotted.
Data, identity and cross-cell queries
State is the hard part. Each cell owns its data, which means anything that spans tenants, such as global search, admin reporting, billing aggregation or a platform-wide user directory, cannot be answered by one cell. There are three accepted answers. Replicate selected data asynchronously to a separate analytics plane that is not in the request path. Accept scatter-gather queries against all cells for rare administrative operations, with the understanding that these inherit the failure of any cell. Or keep a small, highly reliable global directory, such as a tenant-to-cell mapping, and engineer it as the one shared dependency it truly is.
Identity is the classic leak. If login depends on a single global user table, then that table is the real blast radius. Many teams resolve it by making authentication tokens self-contained and verifiable locally, with signing keys distributed to every cell, so a cell can authenticate its own traffic without a call to a shared service. The key distribution path becomes the shared dependency, but it is a low-rate, cacheable one, which is a far safer thing to share than a database.
Trade-offs, Gotchas, and What Goes Wrong
Cells are not free, and teams that adopt them for the wrong reasons often end up with the costs and none of the benefits. Here are the failure modes that recur.
The router becomes the new single point of failure. Every request crosses it. Keep it stateless, cache the mapping, avoid dependencies on the request path, deploy it in more than one failure domain and test its failure. A router with a rich feature set is a router with a rich outage. If you add authentication, rate limiting, request rewriting and A/B testing to the “thin” layer, you have rebuilt the monolith in front of the cells.
Hidden shared dependencies. The most common failure is a cell that quietly calls a global service: a shared configuration store, a shared DNS zone, a shared certificate authority, a shared message bus, a shared cache. Under normal conditions it is invisible. During an incident, it becomes the path along which the failure spreads. Audit by dependency graph and by fault injection: blackhole the shared service from one cell and see what breaks.
Uneven cells and whale tenants. Cell isolation assumes roughly equal cells. A tenant that is 30 percent of your load will saturate a cell on its own, and a hot partition key defeats a hash. Give whales dedicated cells via the override map, monitor per-tenant load, and set per-tenant quotas so that no one can consume more than a defined slice of a cell. This is where per-tenant limiting at the router or cell edge earns its keep.
Retry amplification across cells. If a client sees cell 3 failing and its SDK retries through the router with exponential backoff, that is fine. If upstream services retry aggressively on every layer, a single cell’s failure can generate multiples of its normal load on its neighbours through shared downstreams. Use retry budgets, jittered backoff and circuit breakers, and make sure retries do not cross the cell boundary silently. The Builders’ Library material on shuffle sharding is explicit that its isolation depends on fault-tolerant clients that work around a bad shard.
Operational multiplication. Ten cells means ten of everything: ten databases to patch, ten sets of alerts, ten capacity plans. Without automation, toil scales with N. Cells are only viable with infrastructure as code, automated provisioning and uniform tooling. If a new cell cannot be created by a pipeline in an hour or so, you are not ready to run many of them.
Cost. Per-cell fixed overhead and N+1 headroom are real. The 11 percent spare in a ten-cell design is modest, but a cell that must hold dedicated minimums of everything (three nodes, a replicated database, a cache) multiplies baseline spend. Quantify this honestly against what an hour of full outage costs. For a small system serving a handful of customers, cells are overkill; the pattern pays at scale or where the cost of correlated failure is very high, such as safety, payments or industrial control.
Gray failures. Slack’s 2021 experience is the cautionary example: components disagreed on which backends were healthy, with each side seeing the other as down. Cells reduce the damage because traffic stays within a cell, but only if the cell boundary is also a networking boundary. Siloing, where a service only talks to peers in its own cell, was Slack’s answer, and it turns one big ambiguous failure into several small unambiguous ones. The cost is that siloed capacity cannot be shared, so each silo needs its own headroom.
Cross-cell consistency. Anything that needs a global invariant, such as a unique username across the platform or a global rate limit, cannot be satisfied inside one cell. Design those features deliberately, or accept that they are cell-scoped.
Practical Recommendations
Start from your failure history rather than from the pattern. List your last ten major incidents and mark which would have been contained by a cell boundary. If bad deploys and noisy tenants dominate, cells will pay off. If your failures are mostly third-party outages or regional events, you may want multi-region redundancy first.
Pick the partition key with care, because changing it later means re-platforming. Choose the tenant or workspace identifier that appears on every request, and keep per-tenant overrides from day one so that you can isolate whales without a redesign.
Build the router and the drain button before you build the second cell. A single cell behind a router costs almost nothing and forces you to prove that the thin layer really is thin. Then add cells gradually, using the first as a canary for the others.
Make cell-scoped deployment the default and global deployment the exception that requires approval. Most of the isolation value comes from this single rule.
A short checklist to evaluate a design:
- Can you delete all but one cell and still serve that cell’s tenants correctly?
- Does the router resolve a request using only a locally cached table and the request itself?
- Is every metric, alert, log and trace labelled by cell, with per-cell paging?
- Is there a tested, single-action drain with gradual undrain in small percentage steps?
- Does every deployment and configuration change roll out cell by cell with automatic rollback?
- Have you listed and fault-injected every dependency shared across cells?
- Do you have a rehearsed tenant migration procedure and dedicated cells for whales?
- Are retry budgets and per-tenant quotas in place, with shuffle sharding or similar inside a cell?
- Is capacity headroom (about 1/(N-1) per cell for single-cell loss tolerance) funded and monitored?
The Slack timeline is a useful reality check on effort: roughly a year and a half to migrate an existing system, not a quarter. Adopt cells incrementally, starting with the highest-risk, highest-scale component, and expect the migration of stateful services to dominate the schedule.
Frequently Asked Questions
What is cell-based architecture?
Cell-based architecture runs several identical, fully independent copies of a workload, called cells, each serving a subset of customers chosen by a partition key such as tenant ID. A thin router sends each request to the owning cell, and cells share no state. A failure or bad deployment is contained to one cell, so the blast radius is about 1/N of customers rather than all of them.
How is a cell different from a microservice or a shard?
A microservice splits a system by function, a shard splits only the data, and a cell is a complete copy of the whole workload, including compute, queues and its own data store. Cells are the unit of deployment and failure, so they isolate faults that sharding alone cannot, such as a bad release or a runaway compute path. A microservice system can be deployed as cells, and the two compose well.
How many cells should I run, and how big should each be?
There is no universal number, and AWS guidance does not prescribe one. Pick the largest cell you can fully load-test before production, so that its maximum capacity is a measured fact, then run enough cells that losing one is financially and contractually survivable. Remember that N cells need roughly 1/(N-1) spare capacity each to absorb a single-cell loss, and that very large tenants may need dedicated cells.
What is the difference between cells and shuffle sharding?
Cells give a hard boundary: customers in the same cell share fate. Shuffle sharding gives each customer a random subset of workers, so two customers rarely share the same subset. With 8 workers and subsets of 2 there are 28 combinations, versus 4 shards in fixed sharding. Cells also contain bad deployments, which shuffle sharding cannot, so mature designs often use both.
Does a cell-based architecture replace multi-region or multi-AZ redundancy?
No. Cells contain failures inside a region, including bad deploys and noisy tenants, but a cell that lives in one region still dies with that region. Multi-AZ and multi-region designs protect against infrastructure loss. Cells reduce the scope of impact for everything else. In practice, you deploy cells within a region or zone, and replicate or fail over cells across regions for the data you cannot afford to lose.
What are the biggest risks when adopting cells?
The main risks are a router that grows fat and becomes a single point of failure, hidden dependencies on shared global services, uneven load from whale tenants, and operational toil that scales with the number of cells. Add retry storms that cross cell boundaries and global deployments that bypass staging. Most of these are avoided by keeping the router thin, auditing dependencies with fault injection and automating cell provisioning.
Further Reading
- Multi-region active-active database architecture in 2026: the regional-failure companion to cell isolation, with the data-layer trade-offs cells depend on.
- Distributed rate limiting with token bucket, GCRA, Redis and Envoy: per-tenant limits that keep one customer from exhausting a cell.
- Azure IoT Akri and the Kubernetes resource interface: an edge example where fleets of devices map naturally to cell-scoped partitions.
- Chaos engineering on Kubernetes with Chaos Mesh and Litmus: how to prove that your cell boundaries actually hold.
- AWS Well-Architected whitepaper on cell-based architecture: the canonical definition of cells, routers and control planes.
- Amazon Builders’ Library: Workload isolation using shuffle-sharding: the source of the worker and Route 53 examples.
- Slack Engineering: Slack’s migration to a cellular architecture: a real migration, with drain mechanics and lessons.
By Riju — about
