cerf
Slack

Slack

Slack's cellular architecture, or how to walk away from an availability zone

One flaky network link between AZs in us-east-1 convinced Slack that surviving zone failures means being able to drain a zone in minutes, not diagnose it in hours.

The gray failure that multi-AZ did not save them from

On June 30, 2021, a network link connecting availability zones in AWS us-east-1 started faulting intermittently. Slack was already deployed across multiple AZs, exactly the way the textbooks say, and users still saw slowness and degraded connections. The postmortem vocabulary matters here: this was a gray failure, meaning different components had different views of the availability of the system. Servers inside the impacted AZ saw their local backends as completely healthy while backends in other zones looked unavailable, and servers outside the zone saw the mirror image. When health checks disagree with each other, automated remediation has nothing solid to act on.

Slack's conclusion was to stop trying to automatically diagnose gray failures and instead build a blunt, reliable, human-operated lever: drain the whole zone. Their stated design goals were to remove as much traffic as possible from an AZ within 5 minutes, to do it without user-visible errors, to make drains and undrains incremental, and to guarantee the draining mechanism does not rely on resources in the AZ being drained. That last goal is the AWS static-stability principle in miniature: the recovery path must not depend on the thing that is failing.

The architecture that makes the lever safe to pull is siloing. Slack treats each service as N virtual services, one per AZ. A siloed service only receives traffic from within its AZ and only sends traffic upstream to servers in its AZ, so each zone behaves like a self-contained cell and a failure in one AZ is contained to that AZ. Once nothing meaningful crosses zone boundaries mid-request, removing a zone from service becomes a pure traffic-management decision at the front door.

SlackThe June 2021 gray failure: a regional call graph crossing an impaired AZ link
The June 2021 gray failure: a regional call graph crossing an impaired AZ linkSlack edgeAWS Cloudus-east-1Availability Zone 1Availability Zone 2Availability Zone 3 (impaired)client requestdegradedin-AZ callregional call graphintermittent packet lossin-AZ callin-AZ callreports AZ 3 unavailablereports local healthyreports AZ 1 unavailableSlack clientsEdge loadbalancerspreads across all …App frontendAZ 1BackendAZ 1Health checkviewas seen from AZ 1App frontendAZ 2BackendAZ 2App frontendAZ 3 (impaired)BackendAZ 3 (impaired)Inter-AZnetwork linkintermittent faultsHealth checkviewas seen from AZ 3
Trace
Before siloing, a request that lands in a healthy AZ can still fan out to backends in another zone, so it crosses the intermittently faulting inter-AZ link. The failure is 'gray' because health checks disagree: a checker inside AZ 1 sees AZ 3 as unavailable while a checker inside AZ 3 sees AZ 1 as unavailable but its own backends as perfectly healthy. Automated remediation has nothing solid to act on.

The drain, step by step

Follow a normal request first. A Slack client lands on the edge, where Slack has migrated from HAProxy to the Envoy and xDS ecosystem, so every edge load balancer is an Envoy proxy. Envoy holds a weighted target cluster per availability zone and picks one; from that moment the request stays inside the chosen cell. The app frontend it hits calls only backends in the same AZ, those backends call their own in-AZ upstreams, and the response goes back out. Under healthy conditions the zones simply share load, each cell running the full service stack.

Now the drain. An operator decides AZ 3 looks wrong and triggers a drain. Rotor, Slack's in-house xDS control plane, pushes new weights to every edge Envoy through RTDS, the Envoy runtime discovery service, telling them to reweight their per-AZ target clusters. Propagation through the control plane is on the order of seconds, and weights support gradual drains with a granularity of 1 percent, so an operator can bleed a suspect zone slowly or zero it instantly. In-flight requests complete, new requests land only in healthy cells, and because the control signal originates at the edge, none of it depends on anything running inside the drained zone.

Why drain at the edge instead of teaching every internal service to shift traffic? Because Slack's services are written in Hack, Go, Java, and C++, and discover each other through the Envoy xDS API, the Consul API, and even DNS. A per-service drain would mean reimplementing the logic in every language and every discovery mechanism, and maintaining internal forks of open-source systems along the way. Concentrating the mechanism at the edge means one implementation covers everything siloed behind it. The documented exception is stateful storage: Vitess, Slack's main datastore, offers strongly consistent semantics, and each shard has a single primary that must accept all writes for that shard. If a frontend's local AZ does not hold the primary, the write crosses zones, and if a primary becomes unavailable, writes to that shard fail until it returns or a replica is promoted. Consistency, not laziness, is what keeps the data tier from being fully siloed.

SlackDraining a zone at the edge: Envoy weights, Rotor/RTDS, and the Vitess exception
Draining a zone at the edge: Envoy weights, Rotor/RTDS, and the Vitess exceptionSlack edge control planeAWS Cloudus-east-1Cell: AZ 1Cell: AZ 2Cell: AZ 3 (draining)client TLSRTDS weight push (seconds, 1% granularity)weight 50weight 50weight 0 (draining)xDS/Consul/DNSxDS/Consul/DNSxDS/Consul/DNSin-AZ onlyin-AZ onlyin-AZ onlywrite to in-AZ primarywrite crosses AZ to primaryasync replicationSlack clientsEnvoy edgeproxiesweighted per-AZ tar…RotorxDS control planeServicediscoveryxDS API / Consul / …App frontendAZ 1 siloBackendAZ 1 siloVitess shardprimaryshard A, in AZ 1App frontendAZ 2 siloBackendAZ 2 siloVitess replicashard A, in AZ 2App frontendAZ 3 silo (draining)BackendAZ 3 silo (draining)
Trace
Every edge load balancer is an Envoy proxy holding a weighted target cluster per AZ; once a cell is chosen the request stays inside it. To drain AZ 3, Rotor pushes new weights over RTDS in seconds at 1 percent granularity, so new requests land only in healthy cells while in-flight ones complete. Internal services find each other over xDS, Consul, and DNS, which is why the drain lever lives at the edge instead of in every service. Vitess is the documented exception: a write must reach the single shard primary even when it sits in another AZ.

What this teaches for the exam

For task 1.3, Slack's per-AZ weighted clusters are a self-built version of controls that ELB gives you off the shelf. Cross-zone load balancing is the exact knob: with it disabled, each load balancer node distributes traffic only to targets in its own AZ, which is Slack-style zonal affinity; with it enabled, every node sprays across all zones and you lose fault isolation in exchange for evenness. NLB disables cross-zone by default, ALB enables it by default and lets you turn it off per target group. The managed equivalent of Slack's drain lever is zonal shift in Amazon Application Recovery Controller: for supported resources such as ALBs and NLBs, you shift traffic away from an impaired AZ to the healthy zones in the same Region, with an expiry from one minute up to three days, and AWS tells you to prescale before shifting because the surviving zones absorb the displaced load. Slack's 5-minute goal and incremental weights are the same idea with more granularity.

For task 3.1, this study is really about intra-Region communication patterns. The Amazon Builders' Library makes the mathematical case Slack lived through: if regional services call other regional services, a request has multiple chances to touch the impaired zone, but zone-local call graphs mean a request that starts in a healthy AZ stays healthy end to end. That is why AWS keeps foundational data-plane components like the NAT gateway strictly zonal. Two testable habits fall out. First, know which AWS resources are zonal versus regional, because AZ-independent designs are built from zonal pieces: subnets, NAT gateways, and load balancer nodes per AZ. Second, apply the static-stability rule to any recovery design the exam offers you: a failover or drain mechanism that depends on control-plane actions inside the failing zone, or on capacity you have not already provisioned, is the wrong answer. Slack wrote that requirement down explicitly, and so does AWS.

SlackThe managed equivalents: cross-zone-off ELB, zonal NAT, and ARC zonal shift
The managed equivalents: cross-zone-off ELB, zonal NAT, and ARC zonal shiftAWS Cloudus-east-1VPC 10.0.0.0/16Availability Zone 1Availability Zone 2Availability Zone 3 (impaired)DNS resolveAZ 1 node IPAZ 2 node IPremoved by zonal shiftin-AZ targets onlyin-AZ targets onlydrainedegress via zonal NATegress via zonal NATfails with the AZshift AZ 3 outprescaleprescaleClientsRoute 53resolves the ALB DN…ApplicationRecovery…zonal shiftAuto Scalingprescale surviving …ALB nodeAZ 1, cross-zone offTargetsAZ 1NAT gatewayAZ 1 (zonal)ALB nodeAZ 2, cross-zone offTargetsAZ 2NAT gatewayAZ 2 (zonal)ALB nodeAZ 3 (shifted out)TargetsAZ 3 (impaired)NAT gatewayAZ 3 (zonal)
Trace
What Slack built by hand, AWS gives off the shelf. Disabling cross-zone load balancing makes each ALB/NLB node send only to targets in its own AZ, reproducing zonal affinity. A zonal shift in Amazon Application Recovery Controller is the managed drain lever: it pulls the impaired AZ out of the load balancer, but you must prescale the surviving zones first because they absorb the displaced load. NAT gateways stay strictly zonal, so AZ-independent designs are assembled from per-AZ pieces.

More diagrams

Slack's AZ-siloed cells with edge-controlled draining
Slack's AZ-siloed cells with edge-controlled drainingSlack edgeAWS Cloudus-east-1Cell: AZ 1Cell: AZ 2Cell: AZ 3 (drained)RTDS weight pushweight 50weight 50weight 0in-AZ onlyin-AZ onlyin-AZ onlywrites cross AZSlack clientsEnvoy edgeproxiesweighted per-AZ clu…RotorxDS control planeApp frontendsAZ 1 siloBackendsAZ 1 siloApp frontendsAZ 2 siloBackendsAZ 2 siloApp frontendsAZ 3 siloBackendsAZ 3 siloVitessshard primaries, no…
Trace
Envoy at the edge holds a weighted cluster per availability zone. Rotor, Slack's xDS control plane, pushes new weights via RTDS to drain a zone; inside the region, siloed services only talk to servers in their own AZ. Vitess shard primaries are the documented exception and still receive cross-AZ writes.

Sources

Practice what you just read

6 questions from this architecture

Loading…