Your service can have three Availability Zones, twelve healthy targets, and a ton of unused capacity, yet overload when one zone disappears. Why? Because you haven't told us where the spare capacity is or how to access it.
I want to work through that detail with an example. Say 9,000 requests per second hit a load balancer in us-east-1, with targets spread 2/4/6 across zones A, B, and C.
Tracing one request, we start with DNS helping the client choose a load-balancer endpoint. That endpoint picks a target. Cross-zone load balancing changes the second decision. It doesn't promise an even first decision, replace a failed connection, or launch another server.
My cell-based architecture article was about containing failure. Here, the question is more specific: after we stop using one zone, will the remaining capacity be able to handle the load?
Two routing decisions, one uneven fleet
For this model, you have one target group with identical EC2 instance targets. Every target has an assumed sustainable capacity of 1,200 requests/second at the required latency. That number is an input, not a measured benchmark. All requests cost the same; all targets start healthy. Initially, exactly one-third of demand enters each zone's load-balancer endpoints.
If cross-zone routing is enabled, every endpoint can select healthy targets across the enabled zones. With it disabled, each endpoint selects only local targets. AWS documents this distinction for ALB target groups and NLB.
| Target zone | Healthy targets | Ingress at that zone (req/s) | Cross-zone off: offered req/s per local target | Cross-zone on: offered req/s per target |
|---|---|---|---|---|
| A | 2 | 3,000 | 1,500 | 750 |
| B | 4 | 3,000 | 750 | 750 |
| C | 6 | 3,000 | 500 | 750 |
With cross-zone off, each zone receives one-third of the work regardless of its target count. With it on, the work is split across 12 targets: 9,000 / 12 = 750. A's targets collectively receive 1,500 requests/sec, B's receive 3,000, and C's receive 4,500. The ingress column describes endpoints, not target destinations.
The product details matter:
| Behavior | Application Load Balancer | Network Load Balancer |
|---|---|---|
| Load-balancer default | Cross-zone enabled and cannot be disabled there | Cross-zone disabled |
| Target-group control | Inherits by default; can explicitly disable | Inherits by default; can override on or off |
| Unit selected | HTTP request, subject to routing settings | For TCP, a connection selected by flow hash |
The table assumes ALB round robin with stickiness and slow start off. These are offered loads; an overloaded target may not serve every request. For NLB, treat the numbers as an expected allocation over many independent, equally busy TCP connections. NLB keeps each TCP connection on one target. A connection carrying thousands of requests won't be split into thousands of balancing decisions.
A is already overloaded with cross-zone off. Regional capacity is 12 × 1,200 = 14,400 req/s, but not all of it can be used for A's traffic. That is the difference between spare capacity and routable spare capacity.
Equal ingress is also a simplifying assumption. If A receives 50% of demand, B 30%, and C 20%, local targets receive 2,250, 675, and 300 req/s, respectively. With cross-zone on, each target still receives 750 req/s in the ideal model. In practice, connection reuse, flow sizes, and routing algorithms affect that result.
Remove C and calculate what survives
Chaos Monkey time. Now make C's six targets unavailable. Keep offered demand at 9,000 req/s. First, I'll calculate the load once C's endpoints and targets are out of use, clients have reconnected, and ingress divides equally between A and B. This is the converged state; losing C's targets alone doesn't guarantee that clients have left its endpoints.
| Surviving zone | Targets | Cross-zone off: offered req/s per target | Cross-zone on: offered req/s per target | Assumed target capacity |
|---|---|---|---|---|
| A | 2 | 2,250 | 1,500 | 1,200 req/s |
| B | 4 | 1,125 | 1,500 | 1,200 req/s |
Cross-zone can spread overload evenly, but it cannot solve it. Only 6 × 1,200 = 7,200 req/s of target capacity remains. Without it, B has a little room while A receives nearly twice its capacity.
With cross-zone on, the pooled fleet was only 62.5% utilized before failure. Yet losing one of three zones removed 50% of its capacity. Counting zones hid the placement problem.
For identical targets and ideal pooled routing, let L be peak offered load, q sustainable capacity per target, and nA, nB, nC target counts. Surviving any one-zone loss requires:
q × (nA + nB + nC − max(nA, nB, nC)) ≥ L
With local routing, each surviving zone must independently carry its assigned share. If p_i is zone i's fraction of post-failure ingress:
q × n_i ≥ p_i × L for every surviving zone, for every AZ-loss case
Aggregate capacity is necessary in either design. Local routing adds placement constraints. A 60/40 surviving ingress split, for example, requires capacity for 5,400 req/s in the busier zone, not 4,500.
Redistributing the same 12 targets into 4/4/4 leaves eight after any one-zone loss: 9,600 req/s of capacity for 9,000 req/s of work. Each surviving target handles 1,125 req/s under even routing. No new targets, but a different failure outcome. Only 600 req/s of total margin remains, which retries or a deployment can consume quickly.
The familiar three-zone rule follows from equal placement: after a failure, each surviving zone must carry half the peak workload, so total capacity must be at least 1.5 times demand. That's 50% extra relative to demand, or one-third of installed capacity unused. This matches AWS's static stability guidance. It's a floor under those assumptions, not a universal utilization target. Use tested sustainable capacity, include skew and retry load, and subtract deployment unavailability before checking the inequality.
This is not a hypothetical risk. In its October 2025 us-east-1 incident report, AWS describes NLB health-check instability that triggered automatic AZ DNS failover and removed capacity. Applications saw connection errors when surviving healthy capacity couldn't carry demand. The regional event doesn't prove that either cross-zone setting would have prevented it. It does show why removing unhealthy-looking capacity and preserving service are separate questions.
Choose the boundary you can operate
If deployments regularly leave uneven target pools and sharing capacity matters, I would leave cross-zone on. I would pair it with a rehearsed ARC zonal shift. Strict locality makes sense when the rest of the request path also stays local and each surviving zone has its own reserve. Keeping the load-balancer-to-target hop local won't isolate a service that immediately calls dependencies in another zone.
Cost can change the choice. The AWS ELB FAQ distinguishes ALB's uncharged cross-zone regional transfer from NLB's billable inter-zone transfer. Load-balancer and capacity-unit charges still apply.
For an internal NLB, disabling cross-zone keeps the NLB-to-target leg within a zone. Client-to-NLB traffic can still cross zones. AWS's April 2026 cost analysis explains the separation of those legs and DNS affinity. I would weigh the savings against the extra local reserve and operational work before choosing locality on cost alone.
Conclusion
I would run the same capacity calculation with each zone down in turn before changing the setting. Then rehearse evacuation with realistic connection reuse and a deployment in progress. Measure surviving per-target load, errors, latency, reconnect time, and healthy-target counts by zone. Keep enough capacity running through the recovery interval; scaling after failure is useful recovery work, but it isn't spare capacity at failure time.
Do the surviving targets stay within their tested limits while clients move? If a surviving zone overloads during the drill, change target placement, reserve capacity, or routing before treating the zonal boundary as dependable.


