Back to blog
AWSBest PracticesCloud SecurityNetworkingReliability

Load Balancer Has Single Target: Fixing Non-Redundant Target Groups on AWS

Learn why an AWS target group with a single registered target is a hidden single point of failure, and how to add redundancy with CLI, Terraform, and CI/CD gates.

TL;DR

This check flags any target group behind a load balancer that has only one registered, healthy target. A single target means no redundancy, so one instance failure takes the whole service down. Register at least two targets across separate Availability Zones to fix it.

A load balancer exists to spread traffic across multiple backends and survive the loss of any one of them. When a target group sits behind your ALB or NLB with exactly one target registered, you have a load balancer that cannot balance anything. You are paying for the LB hourly charge and getting none of the availability benefit it promises.

The lb_nonredundant check in the lb_checks module catches this configuration before it bites you during a deploy, a hardware failure, or an AZ outage.


What this check detects

Lensix inspects every target group attached to your Application, Network, and Gateway Load Balancers. For each target group, it counts the number of registered targets. If a target group has only one target, the check raises a finding.

This applies regardless of target type:

  • instance targets (EC2 instances registered by ID)
  • ip targets (raw IP addresses, common with ECS, Fargate, or on-prem backends)
  • lambda targets (a single function is expected here, so context matters)

Note: A target group with a single Lambda target is normal and usually fine, since Lambda handles its own concurrency and availability. The check is most meaningful for instance and ip target types, where one target equals one point of failure.


Why it matters

The whole point of putting a load balancer in front of a service is fault tolerance and horizontal scaling. A single target undermines both.

One failure equals full outage

If your only target fails its health checks, crashes, or gets terminated, the load balancer has nowhere to route requests. Clients receive 5xx errors. With two or more targets in different Availability Zones, the LB simply stops sending traffic to the unhealthy one and your service keeps serving.

Deploys become downtime

Rolling deployments, AMI replacements, and instance refreshes all rely on having spare capacity to take traffic while one target is being replaced. With a single target, every deploy is effectively a hard restart. There is no instance to absorb requests during the swap.

AZ outages take you offline

AWS recommends spreading workloads across at least two AZs. A lone target lives in exactly one AZ. When that AZ has problems, and AZs do have problems, your service goes dark even though the rest of the region is healthy.

Warning: A single target also defeats connection draining and graceful shutdown. Even a clean, planned termination will drop in-flight requests because there is no peer to hand them off to.

The silent cost angle

An ALB costs money every hour plus LCU charges whether it fronts one target or fifty. If you are running a single-target setup, you may be better off skipping the load balancer entirely, or you are accidentally leaving a service under-provisioned. Either way it is worth a look.


How to fix it

The fix is to register more targets, ideally in different Availability Zones. The exact steps depend on how your targets are managed.

Step 1: Identify the target group and current targets

aws elbv2 describe-target-health \
  --target-group-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/web-tg/abc123 \
  --query 'TargetHealthDescriptions[].{Id:Target.Id,AZ:Target.AvailabilityZone,State:TargetHealth.State}' \
  --output table

If this returns a single row, you have confirmed the finding.

Step 2a: If targets come from an Auto Scaling Group

This is the right long-term answer. Let the ASG manage capacity and register targets automatically. Bump the minimum and desired capacity to at least two and ensure the ASG spans multiple subnets.

aws autoscaling update-auto-scaling-group \
  --auto-scaling-group-name web-asg \
  --min-size 2 \
  --desired-capacity 2 \
  --vpc-zone-identifier "subnet-0aaa111,subnet-0bbb222"

The two subnets should belong to different Availability Zones. The ASG will launch the second instance and register it with the target group automatically.

Step 2b: If you register targets manually

Launch a second instance in another AZ, then register it.

aws elbv2 register-targets \
  --target-group-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/web-tg/abc123 \
  --targets Id=i-0newinstanceaz2

Step 2c: If targets are ECS or Fargate tasks

Increase the desired count on the service. The ECS scheduler registers and deregisters tasks with the target group as they start and stop.

aws ecs update-service \
  --cluster prod \
  --service web \
  --desired-count 2

Make sure the service's network configuration includes subnets in more than one AZ so the tasks actually spread out.

Step 3: Verify the load balancer spans multiple AZs

Adding targets in a second AZ only helps if the load balancer itself is enabled in that AZ. Check and add subnets if needed.

aws elbv2 set-subnets \
  --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:loadbalancer/app/web-lb/def456 \
  --subnets subnet-0aaa111 subnet-0bbb222

Tip: Turn on cross-zone load balancing so traffic is distributed evenly across all targets regardless of AZ. It is on by default for ALBs but off by default for NLBs.

aws elbv2 modify-target-group-attributes \
  --target-group-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/web-tg/abc123 \
  --attributes Key=load_balancing.cross_zone.enabled,Value=true

Fixing it in Terraform

If you manage infrastructure as code, set capacity in the resource definition rather than poking at the live environment.

resource "aws_autoscaling_group" "web" {
  name                = "web-asg"
  min_size            = 2
  desired_capacity    = 2
  max_size            = 6
  vpc_zone_identifier = [
    aws_subnet.private_az1.id,
    aws_subnet.private_az2.id,
  ]
  target_group_arns = [aws_lb_target_group.web.arn]

  launch_template {
    id      = aws_launch_template.web.id
    version = "$Latest"
  }
}

Warning: Adding a second instance or task roughly doubles the compute cost for that service. This is the price of redundancy and it is almost always worth paying for anything serving production traffic, but budget for it.


How to prevent it from happening again

Manual fixes drift back. Bake redundancy into the systems that create your infrastructure.

Enforce minimum capacity in policy-as-code

Catch single-instance ASGs and single-task ECS services at pull request time. Here is an OPA Rego rule for a Terraform plan check that rejects ASGs with a desired capacity below two.

package terraform.asg

deny[msg] {
  rc := input.resource_changes[_]
  rc.type == "aws_autoscaling_group"
  cap := rc.change.after.desired_capacity
  cap < 2
  msg := sprintf("ASG '%s' has desired_capacity %d; minimum is 2 for redundancy", [rc.address, cap])
}

Gate it in CI/CD

Run the policy check as a required step before terraform apply or your deploy job. A failing redundancy check should block the merge, not generate a ticket someone closes later.

Tip: Lensix runs lb_nonredundant continuously, so even resources created outside your IaC pipeline (console clicks, emergency fixes, other teams) get caught. Wire the findings into your alerting channel so a single-target group surfaces within minutes rather than during the next incident.

Use AZ rebalancing and minimum healthy targets

For ECS, set a deployment configuration with minimumHealthyPercent at 100 and maximumPercent above 100 so the scheduler never drops below your running count during deploys. For ASGs, enable instance maintenance policies that keep capacity available during refreshes.


Best practices

  • Two AZs minimum, three where it counts. For anything customer-facing, spread targets across at least two Availability Zones. Critical systems benefit from three.
  • Right-size, do not under-size. If a single small instance handles your load, run two smaller instances instead of one larger one. You get redundancy and the same total capacity.
  • Health checks should be meaningful. A target group with two targets only helps if health checks accurately detect a bad target. Point them at an endpoint that actually exercises your application, not a static file.
  • Question the lone-target load balancer. If a service genuinely only ever needs one backend and that is acceptable, consider whether you need a load balancer at all. If it needs an LB, it needs redundancy.
  • Test failure, do not assume it. Terminate a target in a non-production environment and confirm the LB keeps serving. Redundancy you have never exercised is a hope, not a guarantee.

A load balancer with one target is a single point of failure wearing a high-availability costume. Give it a second target and it starts earning its keep.