This check flags load balancers attached to a single availability zone, which means one AZ outage takes down all your traffic. Attach subnets from at least two AZs to your load balancer and confirm targets exist in each.
An AWS load balancer is supposed to be the part of your stack that shrugs off failure. The whole point is to spread requests across healthy targets and route around problems. But a load balancer can only route traffic to the zones it has been told about, and if you have wired it into a single availability zone, you have quietly turned your resilience layer into a single point of failure.
The lb_highlyavailable check in the lb_checks module looks at each Elastic Load Balancer in your account and verifies that it spans more than one availability zone. When it finds one bound to a single AZ, it raises a finding.
What this check detects
Every AWS load balancer is associated with a set of subnets, and each subnet lives in exactly one availability zone. When you create a load balancer and give it subnets from only one AZ, all of its nodes run in that single zone.
This check inspects the availability zones (for Application and Network Load Balancers) or the configured subnets and AZs for Classic Load Balancers. If only one AZ is present, the load balancer is not highly available, and the check fails.
Note: An availability zone is a physically isolated datacenter (or group of datacenters) within an AWS region. AZs have independent power, cooling, and networking, so a failure in one is unlikely to affect another. Spreading resources across AZs is the foundational pattern for high availability on AWS.
The finding applies to all three load balancer types:
- Application Load Balancer (ALB) — Layer 7, HTTP/HTTPS routing
- Network Load Balancer (NLB) — Layer 4, TCP/UDP
- Classic Load Balancer (CLB) — the legacy generation
Why it matters
A load balancer pinned to one AZ defeats its own purpose. If that zone has a problem, every request fails, and it does not matter how many redundant instances you run because the front door is gone.
Here is what that looks like in practice:
- Zone-level outages take you fully offline. AWS AZ disruptions are rare but real. When one happens, a single-AZ load balancer has nowhere to send traffic, so your service returns connection errors or timeouts for the entire duration.
- Cross-zone failover never happens. Even if you have instances running in other AZs, a single-AZ load balancer cannot reach them. The healthy capacity sits idle while users see 5xx errors.
- Maintenance becomes risky. Scaling events, AZ rebalancing, or AWS hardware retirements in that one zone hit you with no buffer.
- It silently breaks SLAs. Many teams promise 99.9% or better availability without realizing their load balancer geometry caps the achievable number far lower.
The frustrating part is that this misconfiguration usually comes from a copy-paste or a Terraform module that only listed one subnet. It looks fine in normal operation, passes every smoke test, and stays invisible until the day an AZ goes dark.
Warning: A load balancer can show as healthy in the console while still being single-AZ. Health checks pass, traffic flows, dashboards are green. The flaw only surfaces during a failure, which is the worst possible time to discover it.
How to fix it
The fix is to attach subnets from at least two availability zones, and ideally three. You also need targets registered in each AZ so there is something to route to when one zone fails.
Step 1: Identify your load balancers and their AZs
For ALBs and NLBs:
aws elbv2 describe-load-balancers \
--query 'LoadBalancers[].{Name:LoadBalancerName,Arn:LoadBalancerArn,AZs:AvailabilityZones[].ZoneName}' \
--output table
For Classic Load Balancers:
aws elb describe-load-balancers \
--query 'LoadBalancerDescriptions[].{Name:LoadBalancerName,AZs:AvailabilityZones}' \
--output table
Any load balancer showing a single AZ is your target for remediation.
Step 2: Find suitable subnets in other AZs
You need a subnet in a different AZ within the same VPC. List them:
aws ec2 describe-subnets \
--filters "Name=vpc-id,Values=vpc-0abc123def456" \
--query 'Subnets[].{Subnet:SubnetId,AZ:AvailabilityZone,Cidr:CidrBlock}' \
--output table
Pick subnets that match the load balancer's scheme. Use public subnets for internet-facing load balancers and private subnets for internal ones.
Step 3: Add the additional AZ
For an ALB or NLB, set the full list of subnets you want it to use. This is an absolute set, so include the existing subnet plus the new ones:
aws elbv2 set-subnets \
--load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:loadbalancer/app/my-alb/abc123 \
--subnets subnet-aaa111 subnet-bbb222 subnet-ccc333
Warning: Network Load Balancers treat AZ changes carefully. You can add AZs to an NLB, but you cannot remove an AZ once it is enabled, and adding one may briefly affect connections. Plan NLB changes during a maintenance window and validate with your traffic in mind.
For a Classic Load Balancer, enable the additional AZ:
aws elb enable-availability-zones-for-load-balancer \
--load-balancer-name my-classic-lb \
--availability-zones us-east-1a us-east-1b us-east-1c
Step 4: Make sure targets exist in each AZ
Spreading the load balancer across AZs only helps if there are healthy targets in those zones. Check your Auto Scaling group or target group:
aws autoscaling describe-auto-scaling-groups \
--query 'AutoScalingGroups[].{Name:AutoScalingGroupName,AZs:AvailabilityZones,Subnets:VPCZoneIdentifier}' \
--output table
If your ASG only spans one AZ, update it to use subnets in the same AZs as the load balancer:
aws autoscaling update-auto-scaling-group \
--auto-scaling-group-name my-asg \
--vpc-zone-identifier "subnet-aaa111,subnet-bbb222,subnet-ccc333"
Tip: Turn on cross-zone load balancing so requests are distributed evenly across all registered targets regardless of which AZ they live in. It is on by default for ALBs and free, but off by default for NLBs (where cross-AZ data transfer is billed). Weigh the cost against the smoother distribution.
Fixing it in Terraform
Most single-AZ load balancers are born in infrastructure code that listed one subnet. Fix it at the source so it stays fixed:
resource "aws_lb" "app" {
name = "my-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
# Reference multiple subnets across AZs, not just one
subnets = [
aws_subnet.public_a.id,
aws_subnet.public_b.id,
aws_subnet.public_c.id,
]
}
A cleaner pattern is to derive subnets from a data source so you cannot accidentally hardcode a single zone:
data "aws_subnets" "public" {
filter {
name = "vpc-id"
values = [var.vpc_id]
}
tags = {
Tier = "public"
}
}
resource "aws_lb" "app" {
name = "my-alb"
load_balancer_type = "application"
subnets = data.aws_subnets.public.ids
}
How to prevent it from happening again
Remediating one load balancer is easy. Stopping the next one from shipping single-AZ is where the real work is. Push the guardrail as far left as you can.
Policy-as-code in CI
Catch single-subnet load balancers before they merge. With Checkov on your Terraform plan:
checkov -d . --framework terraform
For a custom rule, an OPA/Conftest policy that requires at least two subnets is direct and readable:
package main
deny[msg] {
resource := input.resource.aws_lb[name]
count(resource.subnets) < 2
msg := sprintf("Load balancer '%s' must span at least 2 subnets across AZs", [name])
}
Wire that into your pipeline so a pull request that introduces a single-AZ load balancer fails the build.
Service Control Policies and config rules
AWS Config has a managed rule, alb-desync-mode-check and related networking rules, but for AZ coverage the most reliable approach is a custom Config rule backed by a Lambda that inspects AvailabilityZones on each load balancer and marks it noncompliant if the count is below two.
Tip: Instead of building and maintaining custom Config rules and CI policies yourself, let Lensix run the lb_highlyavailable check continuously across your accounts and surface single-AZ load balancers automatically. You get the finding without owning the detection logic.
Standardize on modules
If teams build load balancers from a shared, reviewed Terraform module that always pulls multiple subnets, the failure mode disappears for everyone downstream. Centralizing this kind of config is one of the highest-leverage reliability investments you can make.
Best practices
- Use three AZs where the region supports it. Two AZs survive one failure, but three give you more headroom and smoother capacity rebalancing.
- Match targets to load balancer AZs. A load balancer in three AZs with instances in one is still fragile. Keep your Auto Scaling group and your load balancer aligned.
- Enable cross-zone load balancing on ALBs. It evens out traffic and prevents one zone from being overloaded when target counts differ.
- Test failure, do not assume it. Use fault injection (for example AWS Fault Injection Simulator) to simulate an AZ disruption and confirm traffic actually shifts.
- Watch per-AZ health metrics. CloudWatch exposes healthy host counts per AZ. Alarm on a zone dropping to zero healthy targets so you know before customers do.
- Prefer ALB or NLB over Classic. The newer generations have better AZ handling, more features, and a clearer upgrade path.
A load balancer that lives in one availability zone is not redundancy, it is a delayed outage with a countdown you cannot see. Spreading it across AZs is one of the cheapest reliability wins available on AWS.
Spread your load balancers, align your targets, gate the config in CI, and the single-AZ failure mode stops being something you have to think about.

