Skip to content
AWS aws load-balancing 6 min read

Target Groups

A load balancer needs to know where to send incoming traffic. In AWS Elastic Load Balancing (ELB — the managed service that spreads traffic across many servers), that “where” is a target group. A target group is a named bucket of backend destinations (called targets) plus the rules for checking whether each one is healthy. Your load balancer’s listener forwards requests to a target group, and the target group decides which healthy target actually serves the request. Getting target groups right is the difference between a smooth deployment and one where traffic silently disappears.

What a target group actually is

Think of a target group as three things bundled together:

  1. A list of registered targets — the actual backends that will receive traffic.
  2. A protocol and port — how the load balancer talks to those targets (for example HTTP on port 80).
  3. A health check — a probe the load balancer runs against each target to decide if it should receive traffic.

One load balancer can route to many target groups (for example /api/* to one group and everything else to another), and one target group can be shared by multiple load balancers.

Target types — instance vs IP vs Lambda

When you create a target group you must pick a target type, and you cannot change it later. This choice controls what kind of backend you can register.

Target typeWhat you registerBest forNotes
instanceEC2 instance IDs (e.g. i-0a1b2c3d4e5f)Classic EC2 fleets, Auto Scaling GroupsLoad balancer uses the instance’s primary private IP
ipPrivate IPv4/IPv6 addressesContainers (ECS/EKS), on-prem servers reached over VPN/Direct Connect, microservicesLets you target backends outside the VPC’s normal EC2 flow
lambdaA single Lambda functionServerless HTTP backends behind an Application Load BalancerALB only; NLB cannot invoke Lambda

When to use which: Use instance for ordinary EC2 servers, especially with Auto Scaling. Use ip when you run containers or need to reach IPs that aren’t plain EC2 instances. Use lambda only with an Application Load Balancer (ALB — the Layer-7 load balancer that understands HTTP) when you want a serverless backend.

Creating a target group

Console steps

  1. Open the EC2 console, and in the left menu under Load Balancing choose Target Groups.
  2. Click Create target group.
  3. Choose a target type (Instances, IP addresses, or Lambda function).
  4. Enter a Target group name, for example web-tg.
  5. Set the Protocol and Port the load balancer uses to reach targets, for example HTTP and 80.
  6. Pick the VPC (your virtual network), for example vpc-0a1b2c3d.
  7. Expand Health checks and set the protocol and path (for example /health).
  8. Click Next, register your targets, then Create target group.

CLI equivalent

aws elbv2 create-target-group \
  --name web-tg \
  --protocol HTTP \
  --port 80 \
  --vpc-id vpc-0a1b2c3d \
  --target-type instance \
  --health-check-protocol HTTP \
  --health-check-path /health \
  --healthy-threshold-count 3 \
  --unhealthy-threshold-count 2

Output:

{
    "TargetGroups": [
        {
            "TargetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/web-tg/73e2d6bc24d8a067",
            "TargetGroupName": "web-tg",
            "Protocol": "HTTP",
            "Port": 80,
            "VpcId": "vpc-0a1b2c3d",
            "TargetType": "instance",
            "HealthCheckPath": "/health"
        }
    ]
}

Registering and deregistering targets

Creating the group is empty until you put targets in it. Registering a target tells the load balancer to start health-checking it and (once healthy) sending it traffic.

Console steps

  1. Open Target Groups and click your group (web-tg).
  2. Go to the Targets tab and click Register targets.
  3. Select the instances (or enter IPs), set the port, and click Include as pending below.
  4. Click Register pending targets.

CLI equivalent

aws elbv2 register-targets \
  --target-group-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/web-tg/73e2d6bc24d8a067 \
  --targets Id=i-0a1b2c3d4e5f Id=i-0f9e8d7c6b5a

To check status:

aws elbv2 describe-target-health \
  --target-group-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/web-tg/73e2d6bc24d8a067

Output:

{
    "TargetHealthDescriptions": [
        {
            "Target": { "Id": "i-0a1b2c3d4e5f", "Port": 80 },
            "TargetHealth": { "State": "healthy" }
        },
        {
            "Target": { "Id": "i-0f9e8d7c6b5a", "Port": 80 },
            "TargetHealth": {
                "State": "unhealthy",
                "Reason": "Target.FailedHealthChecks",
                "Description": "Health checks failed"
            }
        }
    ]
}

To remove a target (for example before terminating an instance), deregister it:

aws elbv2 deregister-targets \
  --target-group-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/web-tg/73e2d6bc24d8a067 \
  --targets Id=i-0f9e8d7c6b5a

The big gotcha: “unhealthy” usually isn’t the app’s fault

When a target shows unhealthy, the instinct is to blame the application. Most of the time the app is fine — the health check can’t reach it. Always check the health-check configuration first. The two most common causes:

  • A security group blocks the health-check port. The load balancer probes each target on the health-check port. If the target’s security group (a virtual firewall) doesn’t allow inbound traffic from the load balancer’s security group on that port, every check fails and the target is marked unhealthy even though it’s serving traffic perfectly to everyone else.
  • The health-check path returns a non-200 status. A target is only healthy if the path returns a success code (by default 200). If /health returns a 301 redirect, a 403, or a 404 because the route doesn’t exist, the check fails.

Warning: Before debugging your code, run describe-target-health and read the Reason. Elb.RegistrationInProgress just means “wait.” Target.Timeout almost always means a security group or network ACL is blocking the probe. Target.ResponseCodeMismatch means the path returned the wrong HTTP status — fix the path or the expected Matcher.

A quick fix for the security-group case — allow the load balancer’s security group to reach the targets:

aws ec2 authorize-security-group-ingress \
  --group-id sg-0a1b2c3d \
  --protocol tcp \
  --port 80 \
  --source-group sg-0e1f2a3b

You can also widen the accepted status codes if your health endpoint legitimately returns something other than 200:

aws elbv2 modify-target-group \
  --target-group-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/web-tg/73e2d6bc24d8a067 \
  --matcher HttpCode=200,301

Defining a target group as code

Target groups are easy to drift between environments, so defining them in infrastructure-as-code keeps them consistent. Here it is in CloudFormation:

WebTargetGroup:
  Type: AWS::ElasticLoadBalancingV2::TargetGroup
  Properties:
    Name: web-tg
    VpcId: vpc-0a1b2c3d
    Protocol: HTTP
    Port: 80
    TargetType: instance
    HealthCheckPath: /health
    HealthCheckIntervalSeconds: 15
    HealthyThresholdCount: 3
    UnhealthyThresholdCount: 2
    Matcher:
      HttpCode: "200"

Cost note

Target groups themselves are free — you only pay for the load balancer and its capacity units (LCUs). A standalone Application Load Balancer is about $0.0225 per hour (roughly $16/month) plus LCU charges, regardless of how many target groups you attach to it. So splitting traffic into several target groups costs nothing extra.

Best Practices

  • Use a dedicated health-check path like /health that checks dependencies (database, cache) rather than reusing /, which may return 200 even when the app is broken.
  • Always allow the load balancer’s security group inbound on the health-check port — reference it as a source security group, not a raw IP range.
  • Keep the target type correct from the start (instance, ip, or lambda) since it cannot be changed after creation.
  • Tune HealthyThresholdCount, UnhealthyThresholdCount, and the interval so transient blips don’t yank a healthy target out of rotation.
  • Set a sensible deregistration delay (connection draining) so in-flight requests finish before a target is removed during deploys or scale-in.
  • Define target groups in CloudFormation or Terraform so dev, staging, and production stay identical.
Last updated June 15, 2026
Was this helpful?