This check flags load balancers with zero registered targets. They forward no traffic but still bill you hourly, and they clutter your inventory with dead infrastructure. Either re-register healthy targets or delete the load balancer.
A load balancer with no targets is the cloud equivalent of paying rent on an empty office. The lights are on, the meter is running, but nobody is inside. AWS keeps charging you the hourly rate for an Application, Network, or Gateway Load Balancer the moment it exists, regardless of whether a single request ever reaches it.
The Lensix lb_unused check (module lb_checks) scans your AWS account for load balancers whose target groups contain no registered targets, meaning the load balancer cannot serve traffic to anything. This post covers what that means, why it costs you money and creates risk, and how to clean it up properly.
What this check detects
An AWS Elastic Load Balancer (ELB) does not route traffic on its own. It forwards requests to targets (EC2 instances, IP addresses, Lambda functions, or containers) that are registered inside one or more target groups. The flow looks like this:
Client → Listener → Listener Rule → Target Group → Registered Targets
The lb_unused check inspects each load balancer in your account and walks its associated target groups. If every target group attached to a load balancer is empty (no registered targets at all), the load balancer is serving nothing. Lensix flags it as unused.
Note: This is different from a load balancer with unhealthy targets. Unhealthy targets are registered but failing health checks, which usually points to an application problem. An empty target group means nothing was ever registered, or everything was deregistered and never cleaned up.
Common ways a load balancer ends up with zero targets:
- An Auto Scaling Group was deleted but its load balancer was left behind
- EC2 instances were terminated and never re-registered
- A blue/green or canary deployment failed midway and left orphaned infrastructure
- A test or staging environment was torn down by hand, missing the load balancer
- Terraform or CloudFormation drift left the LB in place after its targets were removed
Why it matters
Direct cost
Load balancers are not free to keep around. In most regions an Application Load Balancer runs roughly $0.0225 per hour for the base charge, which is about $16 per month before you add LCU (Load Balancer Capacity Unit) charges. A Network Load Balancer is similar. One forgotten load balancer is small. A few dozen across multiple accounts and regions adds up to real money for serving zero traffic.
Warning: The base hourly charge applies even with no targets and no traffic. You are billed for the resource existing, not for what it does. Idle load balancers are one of the most common sources of slow cloud cost creep.
Security and attack surface
An internet-facing load balancer with no targets still has a public DNS name and may still have a listener bound to a port. That means it remains part of your attack surface. If a future deployment accidentally registers the wrong targets, an internal service could become exposed without anyone noticing. Stale, undocumented infrastructure is exactly the kind of thing attackers look for during reconnaissance.
Operational noise
Dead load balancers pollute your inventory. They show up in dashboards, monitoring tools, and security scans, and every engineer who finds one has to stop and ask "is this safe to delete?" Each empty load balancer is a small unanswered question that slows down audits and incident response.
How to fix it
The fix depends on intent. A load balancer with no targets is either a mistake (targets should be there) or garbage (it should be deleted). Confirm which before you act.
Step 1: Identify the empty load balancers
List your load balancers and their ARNs:
aws elbv2 describe-load-balancers \
--query 'LoadBalancers[].{Name:LoadBalancerName,ARN:LoadBalancerArn,Scheme:Scheme,Type:Type}' \
--output table
For a specific load balancer, find its target groups and check whether any targets are registered:
# Find target groups attached to a load balancer
aws elbv2 describe-target-groups \
--load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:loadbalancer/app/my-lb/abc123 \
--query 'TargetGroups[].TargetGroupArn' \
--output text
# Check registered targets for each target group
aws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/my-tg/def456 \
--query 'TargetHealthDescriptions[].Target.Id' \
--output text
If describe-target-health returns nothing, that target group is empty.
Step 2a: Re-register targets (if the LB should be in use)
If the load balancer is meant to be serving traffic, register the correct targets. For instance targets:
aws elbv2 register-targets \
--target-group-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/my-tg/def456 \
--targets Id=i-0abc123def456789 Id=i-0def456abc789012
If the load balancer fronts an Auto Scaling Group, attach the target group to the ASG instead of registering instances by hand. This way scaling events keep the target group in sync automatically:
aws autoscaling attach-load-balancer-target-groups \
--auto-scaling-group-name my-asg \
--target-group-arns arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/my-tg/def456
Step 2b: Delete the load balancer (if it is garbage)
If nobody can justify the load balancer's existence, remove it.
Danger: Deleting a load balancer is irreversible and immediately drops any client connections routed through it. Confirm there is no live traffic first. Check access logs or CloudWatch RequestCount / ActiveFlowCount metrics over the past 30 days before deleting anything in production.
Verify there has been no recent traffic:
aws cloudwatch get-metric-statistics \
--namespace AWS/ApplicationELB \
--metric-name RequestCount \
--dimensions Name=LoadBalancer,Value=app/my-lb/abc123 \
--start-time $(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period 86400 \
--statistics Sum
If that comes back clean, delete the load balancer and then clean up its orphaned target groups:
# Delete the load balancer
aws elbv2 delete-load-balancer \
--load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:loadbalancer/app/my-lb/abc123
# Delete the now-orphaned target groups
aws elbv2 delete-target-group \
--target-group-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/my-tg/def456
Tip: Deleting a load balancer does not delete its target groups, listeners, or associated security groups. Audit those too, or you will leave behind more orphaned resources than you started with.
How to prevent it from happening again
One-off cleanup is fine, but empty load balancers come back unless you change how infrastructure is created and torn down.
Manage load balancers with IaC
When a load balancer and its targets are defined together in Terraform or CloudFormation, tearing down the stack removes both at once. Orphans happen most often when resources are created by hand or when targets and load balancers live in separate, loosely coupled definitions.
A Terraform module that keeps the load balancer, target group, and ASG attachment together:
resource "aws_lb" "app" {
name = "app-lb"
internal = false
load_balancer_type = "application"
subnets = var.subnet_ids
security_groups = [aws_security_group.lb.id]
}
resource "aws_lb_target_group" "app" {
name = "app-tg"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
path = "/healthz"
healthy_threshold = 2
unhealthy_threshold = 2
}
}
resource "aws_autoscaling_attachment" "app" {
autoscaling_group_name = aws_autoscaling_group.app.name
lb_target_group_arn = aws_lb_target_group.app.arn
}
Add a policy-as-code gate
You cannot easily catch an empty load balancer at plan time because target registration is often runtime behavior. Instead, enforce that every load balancer in code has at least one target group and attachment defined. Here is an OPA/Conftest rule that fails a Terraform plan if a load balancer has no target group:
package main
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_lb"
tg_count := count([tg |
tg := input.resource_changes[_]
tg.type == "aws_lb_target_group"
])
tg_count == 0
msg := sprintf("Load balancer '%s' is defined without any target group", [resource.name])
}
Schedule recurring detection
Run the lb_unused check continuously rather than once. A scheduled scan catches load balancers that became empty after deployment, which static IaC analysis cannot see. Pair the finding with a notification so an owner reviews it within a day or two, not a quarter later.
Tip: Tag every load balancer with an owner and environment tag at creation. When a scan flags an empty one, you know immediately who to ask instead of starting a forensic investigation.
Best practices
- Tie load balancers to their targets in code. Co-locate the LB, target groups, listeners, and ASG attachments in one module so teardown removes everything together.
- Prefer ASG target group attachment over manual registration. Auto Scaling keeps the target group accurate through scale-in and scale-out without human action.
- Set deletion protection on production load balancers. This prevents accidental deletion while you investigate, and forces a deliberate step before removal.
- Review CloudWatch traffic metrics before deleting anything. Zero targets plus zero traffic over 30 days is a strong signal it is safe to remove.
- Clean up the whole chain. When you delete a load balancer, also remove its orphaned target groups, listeners, and unused security groups.
- Treat idle infrastructure as a recurring cost and security item, not a one-time cleanup. Empty load balancers reappear as long as deployments fail or environments are torn down by hand.
An empty load balancer is rarely an emergency, but it is a clear sign of drift between what you think is running and what you are actually paying for. Catching them early keeps your bill honest and your attack surface tight.

