A cell-based architecture splits one large service into many complete, independent copies of the stack, called cells, each serving a fixed slice of customers chosen by a partition key, with a thin router in front and a control plane beside it. A bad deploy, a poison request or a runaway tenant then takes down one cell, about 1/N of your customers, instead of everyone. Azure calls the same idea deployment stamps; geodes are the global variant, where every copy can serve every user from replicated data.
Picture it first: one stadium or many exam centres
A national entrance exam has 10 lakh (1 million) candidates. Hold it in one giant stadium and a single failure, a dead PA system, late question papers, a disruption in one row, costs all 10 lakh their morning.
Real exams use hundreds of centres, each complete on its own: hall, invigilators, sealed papers, generator. Your roll number decides your centre. A power cut at one centre affects only the candidates seated there. When registrations grow, the board opens more centres rather than building a bigger stadium, because it knows how to run a 2,000-seat centre. And it tries any new procedure at a few centres first.
Map it back:
- A centre is a cell: a complete copy of the stack, compute and data, serving only its own customers.
- The roll number is the partition key; the admit-card lookup is the cell router.
- The exam board is the control plane: it opens centres, assigns candidates and rolls out changes.
- Seats per centre is the maximum cell size; a few centres first is a deployment wave.
One fleet, one blast radius
Most services start as one fleet: identical app servers behind a load balancer, one database cluster, shared caches and queues. Any server can serve any customer, so spare capacity is pooled and losing one server barely matters.
But some failures travel with a change or a request, not with a server:
- A bad deploy or config push reaches every server the rollout touches.
- A poison request crashes its server; the client retries elsewhere, and the crash walks through the fleet.
- A noisy or attacked tenant fills shared queues, pools and connections for everyone.
- A scale cliff: a lock or index that behaves at today’s size and collapses at the next, untested one.
Availability Zones and regions don’t help here: they contain power and network failures, but your code, config and customers’ requests cross every zone. Deploy to three zones at once and all three fail together.
So ask a different question: when something fails, how many customers can it touch? That number is the blast radius (AWS says scope of impact). Cell-based design treats it as a requirement you choose, like a latency target.
In the interview
AWS’s cell whitepaper frames the choice in one line: is it better for 100% of customers to see a 5% failure rate, or 5% of customers to see 100%? Cells choose the second, because a contained failure is easier to detect, roll back and explain.
What a cell is
A cell is a complete, independent instance of your workload: app servers, workers, caches, queues and its own databases, built from the same template as every other cell, capped at a tested size and serving a subset of customers. Cells share no state and never call each other. In front sits a thin cell router that sends each request to the cell owning its partition key; beside them sits a control plane that creates cells, places customers and deploys.
The partition key decides who shares fate. It should follow the grain of the product, the natural unit that rarely interacts with others: tenant ID for B2B SaaS, account ID for a cloud API, workspace for team chat, merchant ID for a payment gateway’s merchant APIs. It must be in every request or cheaply derived from it. If the router has to query a database to find it, the router is no longer thin.
Cells need not mean more machines: AWS notes that a service on 30 hosts can stay on 30 hosts, regrouped into cells. What you add is the router, a fixed floor per cell (a database cluster each) and tooling.
The router: thin on purpose
The router is the one component every request crosses, so it is the one place where a bug still has a 100% blast radius.
It must extract the key (from a header, path, subdomain or token claim), map it to a cell from an in-memory table and forward; keep serving from the last good table when the control plane is down (static stability); and give each cell its own connection pool and timeout, a bulkhead so one slow cell cannot tie up the router.
It must not run business logic, rewrite or aggregate requests, fan out to several cells, query a database per request, or retry into another cell. AWS’s Well-Architected guidance lists adding complex business or routing logic to the router layer as an anti-pattern.
A lookup can be this small. A fixed number of hash slots means adding a cell changes only which slots point where:
import hashlib
SLOTS = 4096 # fixed forever; only the slot-to-cell table changes
def cell_for(tenant_id: str, table) -> str:
# Pinned tenants: very large customers, tenants mid-migration, quarantine.
if tenant_id in table.overrides:
return table.overrides[tenant_id]
digest = hashlib.sha256(tenant_id.encode()).digest()
slot = int.from_bytes(digest[:8], "big") % SLOTS
return table.slot_to_cell[slot] # e.g. "cell-07"
def refresh(router, store):
# Runs every few seconds. On any error, keep the last good table.
try:
router.table = store.load_latest_version()
except Exception:
router.stale_seconds += 5 # alarm on this; don't fail requests
| Scheme | How it works | You gain | You pay |
|---|---|---|---|
| Full mapping table | One row per key | Place, pin or move any tenant | A large, critical table that needs read-your-writes |
| Prefix or range | Key ranges map to cells | Small table; ranges split easily | Hot ranges; keys must sort usefully |
| Hash mod N | hash(key) % cells |
No state, even spread | Adding a cell moves almost every tenant |
| Fixed slots → cell table | hash(key) % 4096, then a slot table |
Small table; adding a cell moves only chosen slots | Whole slots move, not single tenants |
| Consistent hashing | Keys and cells on a ring | Adding a cell moves about 1/N of keys | Uneven without virtual nodes; less placement control |
Put an override table in front of any scheme, as in the code, for testing, quarantine and very heavy keys; full mapping has one built in.
Never fail over into another cell
When cell 3 returns errors, the tempting fix is to retry in cell 5. Don’t. Cell 5 doesn’t hold that tenant’s data, and if the request is the poison, you have just carried it into a second cell. Recovery happens inside a cell (replicas across zones) or by deliberately migrating tenants, never by spillover in the router.
The control plane: create, place, move, deploy
The control plane does the slow, careful work that keeps the request path simple (more in control plane vs data plane):
- Provision every cell from one infrastructure-as-code template.
- Place each new tenant using every cell’s capacity, current use and per-tenant share, with headroom. AWS’s example: a cell tested at 10,000 transactions per second should not run at that limit.
- Publish the map as a whole, versioned table that routers poll, so a router that missed an update catches up on the next poll (the constant-work idea in backend failure modes).
- Move tenants and deploy cell by cell, both covered below.
AWS puts the split in CAP terms: the control plane should fail rather than publish a wrong map (CP), while routers and cells keep working on what they last knew (AP). A control-plane outage should block signups and moves, never traffic.
Cell size: cap it, test it, add cells
Every cell has a maximum size: load-test one cell past its breaking point, back off to a safe margin, and grow by adding cells, never by growing them. “The whole system at 3× today’s load” is untestable; “one cell at its cap” is a routine test. Route 53’s health checkers work this way: AWS knows how many checks each cell can sustain and adds a cell as one nears that limit.
Three forces set the cap: big enough for your largest tenant, small enough to test at full scale cheaply, big enough for economies of scale. Smaller cells mean a smaller blast radius; larger ones fit big tenants and waste less on per-cell overhead.
A worked example: a B2B SaaS with 40,000 tenants peaks at 90,000 requests per second.
- One cell breaks at 15,000 req/s in load tests. Cap it at 8,000, about 53% of that, leaving room for spikes and tenant growth.
- 90,000 ÷ 8,000 = 11.25, so 12 cells, each with about 1/12 ≈ 8.3% of traffic. The largest tenant, at 3,000 req/s, fits in one.
- Fixed floor per cell: 3 app instances (one per zone), a 3-node database cluster and 2 cache nodes, 8 nodes in all. Twelve cells cost 12 × 8 = 96 nodes before any load-driven scaling.
- Cap cells at 2,000 req/s instead, for a blast radius near 2%: 90,000 ÷ 2,000 = 45 cells, a floor of 45 × 8 = 360 nodes, and the 3,000 req/s tenant no longer fits.
That is the real trade: blast radius against fixed cost, operational load and the size of your biggest customer.
Blast-radius arithmetic
With N equal cells, a failure confined to one cell touches about 1/N of customers. AWS’s example: 10 cells, one fails, and 90% of requests are unaffected. Compare that with the partitions you already have:
| Partition | Typical count | Hit by one unit failing | What it contains |
|---|---|---|---|
| One shared fleet | 1 | 100% | Single-server faults only |
| Regions, users homed per region | 2–4 | 25–50% | Region-wide disasters |
| Availability Zones | 3 | About a third of capacity | Power and network faults in a zone, if services stay zonal |
| Cells | 10–100 | 1–10% of customers | Bad deploys rolled out in waves, poison requests, noisy tenants, scale cliffs |
| Shuffle shards | Thousands of combinations | About 1 in C(n, k), fully | Poison requests and floods from one customer |
Zones and cells combine: make each cell span three zones with spare capacity for one, and a zone failure costs every cell a third of its capacity, while a bad deploy costs one cell.
N cells have N times as many failure events, each 1/N as large, so expected failed requests need not fall. AWS argues they do anyway: capped, tested cells bring fewer surprises and recover faster. Waves add the biggest gain: catch bad changes in the first of 20 cells and they reach 5% of customers, not 100%.
Your real blast radius is set by whatever is still shared: the router, DNS, identity, a global config store. Each is a potential 100% outage.
Shuffle sharding: overlapping hands
Sharding and cells shrink impact linearly: four shards, a quarter of customers per failure. Shuffle sharding shrinks it combinatorially, on the same hardware.
Take eight workers. Instead of four fixed pairs, give each customer its own random pair, like a two-card hand dealt from an eight-card deck. Hands may overlap, but rarely completely. There are C(8, 2) = 8 × 7 ÷ 2 = 28 possible pairs.
Now let customer A’s poison requests take down both of its workers. Every other customer falls into one of three groups:
- Shares both workers: 1 pair in 28, about 3.6%. Down.
- Shares one: 2 × 6 = 12 in 28, about 43%. Alive, if the client retries on its other worker.
- Shares neither: C(6, 2) = 15 in 28, about 54%. Untouched.
The numbers improve fast with more workers or wider hands. Amazon Route 53 arranges its capacity as 2,048 virtual name servers and gives each customer domain four. C(2,048, 4) ≈ 730 billion possible shards: enough to give every domain a unique one and guarantee that no two domains share more than two name servers. A domain under DDoS saturates its four; every other domain keeps at least two, and the target can be moved onto dedicated attack capacity.
Assignment can be stateless: hash the customer ID into a seed and draw k workers, so any client computes the same hand.
import hashlib, random
def shuffle_shard(customer_id: str, workers: list[str], k: int) -> list[str]:
seed = int.from_bytes(hashlib.sha256(customer_id.encode()).digest()[:8], "big")
return random.Random(seed).sample(workers, k) # same customer -> same k workers
Stateless hands can collide, like row F above. AWS’s open-source Route 53 Infima library also has a stateful variant that checks each new shard against those already issued to guarantee a maximum overlap.
It protects against poison requests, floods aimed at one customer and noisy neighbours, provided clients retry across their hand with backoff and a budget (timeouts, retries and backoff). It doesn’t stop a bad deploy (every worker runs the same code), a shared dependency behind the workers, or whole-fleet overload, and it is awkward for stateful workers. Since cells must not share state, AWS’s FAQ places shuffle sharding inside a cell, or across stateless fleets and queues.
Count combinations, not orderings
You will see this example quoted as 56 shards (8 × 7) and, with four workers per shard, 1,680 (8 × 7 × 6 × 5). Those count ordered picks. A shard is an unordered set, so the counts are C(8, 2) = 28 and C(8, 4) = 70. Interviewers notice.
Deploying through cells in waves
Cells give a deployment pipeline a new unit to stage through:
- One box in the first cell. In Amazon’s pipelines a one-box stage typically serves at most 10% of its zone’s or region’s requests; 10% of a cell holding 5% of customers is about 0.5%.
- One cell, ideally a canary cell with internal tenants or synthetic traffic.
- Growing waves of cells, so confidence grows faster than exposure.
- Bake time after every stage. Amazon’s typical pipeline waits at least an hour after each one-box stage, at least 12 hours after the first regional wave and 2–4 hours after later ones, and needs a minimum number of requests first.
- Automatic rollback whenever an alarm fires.
Amazon’s Builders’ Library describes this inside each region: teams split regional deployments into single Availability Zones or “individual internal shards (called cells)”.
Ship config and feature flags through the same waves, because a bad flag is a bad deploy. Cells also give you update rings for free: risk-averse tenants can live on cells that deploy last. What happens inside each stage is in deployment strategies.
Deployment stamps and geodes
Deployment stamps: cells as scale units
A deployment stamp (also called a service unit, scale unit or cell) is an independent copy of the application including its data stores, serving a predefined number of tenants. A region holds one or more, and you add stamps to scale almost linearly. Mechanically, stamps are cells. Azure stresses other reasons to have them: scale limits (connections, host names, sockets, subscription quotas), tenant placement (a big or sensitive customer gets its own stamp, the silo and pool models at stamp level), data residency, and update rings.
Clients reach a stamp by its own DNS name (unit1.eu.myapi.contoso.com) or through a routing service. Azure’s example puts Azure Front Door in front of regional API Management gateways that read a tenant-to-stamp map from globally replicated Azure Cosmos DB (routing requests to tenants). The costs are stated plainly: run at least two stamps, expect cost to rise substantially, write custom logic to move tenants, and aggregate reports across stamps. Stamps aren’t redundant across regions either.
Geodes: every node serves everyone
A geode (geographical node) is a full deployment in one location, behind a global load balancer, on a geo-replicated read-write store such as Azure Cosmos DB. Every geode has all the data, so any geode can serve any user, and traffic goes to the nearest healthy one. Geodes don’t depend on each other, and each one added raises resilience; the pattern aims to survive losing several regions at once.
The trade-offs are those of multi-region active-active: concurrent writes in many places need a consistency model and a merge plan (CAP and PACELC). Azure advises against geodes when data must stay in one region, sessions hold local state, load leans heavily on one region, or a legacy system must be retrofitted.
Geodes isolate by geography, not by tenant. A poison request can be retried in every geode, an unstaged deploy reaches all of them, and a corrupt write replicates everywhere. Geodes buy latency and regional survival; cells and stamps buy a small blast radius. They combine: Azure notes a stamp routing layer can itself be built as geodes.
How the options compare
| Approach | One unit is | It serves | Contains | Main cost |
|---|---|---|---|---|
| Cells (AWS) | A full stack, compute and data, at a capped, tested size | A fixed subset of customers, by partition key | Bad deploys (with waves), poison requests, noisy tenants, scale cliffs | A router to protect, N copies, migration tooling, cross-cell features |
| Deployment stamps (Azure) | A full stack including data stores; a scale unit, often per region | A predefined set of tenants, or one large tenant | The same, plus residency and per-stamp update cadence | Many copies; tenant moves and cross-stamp reporting are hard |
| Geodes (Azure) | A full app per location over a globally replicated store | Any user, any request | Loss of one or more regions; distance latency | Write conflicts and global data cost; no per-tenant isolation |
| Plain sharding | A slice of the database only | The keys in its range | Data size and hot-key limits | The shared app tier still fails as one |
| Multi-region active-passive | A standby copy of everything | Everyone, after failover | A regional disaster | Idle standby, slow and rarely tested failover; a bad deploy can hit both |
The hard parts
Choosing the partition key
The key must be in every request and match the product’s grain. Three traps:
- Giant tenants. AWS suggests a second key dimension, such as tenant plus project, so a tenant that outgrows a cell can span cells along a natural seam.
- Keys that change. Use a stable internal ID, not a name or email address.
- Cross-key products, such as a social graph. Cell the parts that have a grain and run the cross-cutting rest as a separate, hardened service.
Features that span cells
Global search, analytics and admin consoles need data from many cells. Keep them off the synchronous path: each cell publishes change events to a global read-only index or warehouse, so a lagging index degrades search, not the product. When one cell must reach another, AWS recommends going back through the router, never cell to cell. Keep scatter-gather rare: the slowest cell sets its latency, and if most requests need it, the key is wrong.
Moving tenants between cells
AWS’s outline for a live move is clone, flip, redirect, forget: copy the tenant’s data to the target cell and keep it in sync; flip authority to the target, usually behind a seconds-long write pause for that tenant; redirect by updating the router’s override entry; and delete the old copy after a safety window. AWS says to build this from day one; Azure warns that moves need custom logic. See online schema and data migrations.
Global data and N copies
Identity, plans, flags and the cell map are global. A global service called on every request has a 100% blast radius, so replicate read-only copies into each cell and let cells run on a stale copy.
Tag every metric, log line and trace with the cell ID: an average across 20 cells hides one cell at 50% errors behind 2.5%. Build every cell from one template, because a hand-patched cell fails differently. And make “drain this cell” and “move this tenant” buttons, not runbooks.
When the router fails
The router is shared by design, so it is the likeliest way a cellular system still goes 100% down. Keep it stateless with its map in memory, spread across zones with spare capacity, deployed in its own slow waves, and with no per-request dependency. AWS advises building it with a cell’s discipline: known limits, a capped size, its own dashboards.
Start with cell zero and a second cell
Migrating an existing service? AWS suggests treating today’s stack as cell zero: put the router in front, then move traffic to new cells a slice at a time. Run at least two cells from day one, as AWS and Azure both advise, so single-cell assumptions (global sequences, cross-tenant joins, a hard-coded hostname) can’t hide.
Real system: Slack’s zonal cells
In 2023, Slack described moving its most critical user-facing services to cells over about 18 months. The trigger: on 30 June 2021, a network link between one Availability Zone of its main US East region and other zones began failing intermittently. It was pulled after 48 minutes, failed again that evening and was removed for good.
One zone hurt users because a single Slack API request can fan out into hundreds of RPCs across every zone, and strongly consistent stores such as Vitess need each shard’s one primary for writes. Machines disagreed about which zone was healthy: a gray failure.
So Slack made each zone a cell. Siloed services take traffic only from their own zone and call only upstreams in it, so draining a zone needs one lever at the edge: Envoy load balancers, configured by Slack’s in-house xDS control plane, reweight their per-zone clusters. The goals: drain within 5 minutes (a 99.99% SLA allows under an hour of downtime a year), no user-visible errors, steps as small as 1%, and no dependence on anything inside the zone being drained.
Two kinds of cell
Slack’s cells are zonal, and any of them can serve any user; containment comes from draining a bad cell, not from partitioning customers. That handles infrastructure and gray failures in a zone. A poison request or bad deploy that reaches every zone needs tenant-partitioned cells, waves or both.
Cheat sheet
| Idea | Remember it as |
|---|---|
| Cell | A full copy of the stack, compute and data, capped in size, serving a subset of customers; shares nothing |
| Partition key | Follows the product’s grain; in every request; stable; tenant or account ID is typical |
| Router | Key → cell → forward; in-memory map, last good table, override table; no logic, no cross-cell retries |
| Control plane | Provisions, places, moves, publishes the map, deploys in waves; may fail, must not mislead |
| Size | Load-test to breaking point, cap below it, grow by adding cells |
| Blast radius | About 1/N per cell; 10 cells leave 90% untouched |
| Shuffle sharding | C(n, k) hands; 2 of 8 gives 28; full overlap about 1 in 28; clients retry within their hand |
| Waves | One box → one cell → growing waves; bake, auto-rollback; config too |
| Stamps vs geodes | Stamps are Azure’s cells (scale units, placement, residency); geodes serve any user from replicated data: regional survival, not tenant isolation |
How to say it in the interview
“A shared fleet has a 100% blast radius for bad deploys, poison requests and noisy tenants, so I’d split it into cells: full copies of the stack with their own databases, capped at a load-tested size, say 8,000 requests a second, and grown by adding cells. Tenants map to cells by tenant ID through a slot table with overrides. The router only looks up the cell, from an in-memory map it keeps even if the control plane is down. The control plane places tenants and deploys one box, one cell, then growing waves with bake time. With 12 cells, a failure hits about 8% of tenants; shuffle sharding inside a cell shrinks a poison tenant’s reach further.”
How big should a cell be? Big enough for the largest tenant, small enough to load-test to breaking point cheaply, big enough that the per-cell floor isn’t wasteful. Cap well below breaking point; add cells, don’t grow them.
What goes in the router? Key extraction, an in-memory lookup with overrides, per-cell pools and timeouts, forwarding. No business logic, database calls, fan-out or cross-cell retries, and it must survive a control-plane outage on its last good map.
What if a tenant outgrows its cell? Give it a dedicated cell through the override table. If it outgrows that, add a second key dimension, such as tenant plus project, so it can span cells. The move itself is clone, flip, redirect, forget.
Cells vs shards? Sharding splits only the data tier, so app servers, caches and deploys are still shared, and a bad build hits every shard’s users. A cell splits the whole stack, deploys included. Cells usually contain shards.
Where to go next
- Bulkheads: the same idea at the size of thread pools and fleets.
- Multi-region and active-active: the geode side of this page and the consistency it demands.
- Deployment strategies: what happens inside each wave.
- Control plane vs data plane: why the control plane may fail but the data plane must not.
- Tenancy models: silo, pool and bridge: dedicated cells for big tenants, shared cells for the rest.
- Routing requests to tenants: where the partition key comes from.
- Noisy neighbour: the tenant flood that shuffle sharding tames.
References and further reading
- Reducing the Scope of Impact with Cell-Based Architecture (AWS Well-Architected whitepaper, 2023)docs.aws.amazon.comThe router, cell and control-plane split, partition-key mapping options, cell sizing forces, placement, the clone-flip-redirect-forget migration, wave deployment and the shuffle-sharding FAQ.
- REL10-BP03 Use bulkhead architectures to limit scope of impact (AWS Well-Architected Framework)docs.aws.amazon.comThe 10-cells, 90%-unaffected arithmetic and the common anti-patterns: unbounded cells, all-cell deploys, shared state and logic in the router.
- Workload isolation using shuffle-sharding (Amazon Builders’ Library)aws.amazon.comSharding versus shuffle sharding on eight workers, the 28 combinations, and Route 53’s 2,048 virtual name servers with four per domain.
- Shuffle Sharding: Massive and Magical Fault Isolation (AWS Architecture Blog)aws.amazon.comWhy clients must retry across their shard, and stateless versus stateful (overlap-guaranteeing) shuffle sharding in the Route 53 Infima library.
- Automating safe, hands-off deployments (Amazon Builders’ Library)aws.amazon.comOne-box stages, deployment to one Availability Zone or cell at a time, growing waves, bake times and automatic rollback.
- Reliability, constant work, and a good cup of coffee (Amazon Builders’ Library)aws.amazon.comRoute 53 health checkers built as cells with tested limits, and pushing whole fixed-size tables instead of streams of changes.
- Deployment Stamps pattern (Azure Architecture Center)learn.microsoft.comStamps as scale units holding a subset of tenants, per-stamp DNS versus a routing service with a replicated tenant map, and the costs of moving tenants.
- Geode pattern (Azure Architecture Center)learn.microsoft.comGeographical nodes that each serve any request over a geo-replicated data store, and when to prefer stamps instead.
- Slack’s Migration to a Cellular Architecture (Slack Engineering, 2023)slack.engineeringThe June 2021 gray failure in one Availability Zone, siloed per-AZ cells, and draining a cell in minutes with Envoy weights.