AWS Elastic Load Balancing

By Vishvesh Patel · DevOps EngineerUpdated September 11, 2026

Choose the right AWS load balancer. ALB vs NLB vs GWLB, target groups, health checks, TLS termination and debugging 502 and 503 errors.

Elastic Load Balancing distributes traffic across targets in multiple Availability Zones and takes unhealthy ones out of rotation. AWS offers three types, and picking the wrong one is a decision you feel months later when you need a feature the other one has.

Choosing a type

Application Load BalancerNetwork Load BalancerGateway Load Balancer
OSI layer7 (HTTP/HTTPS)4 (TCP/UDP/TLS)3 (IP)
Routing onPath, host, header, method, queryPort onlyN/A (transparent)
LatencyLowVery lowAdds a hop
Static IPNo (DNS name only)Yes, one Elastic IP per AZN/A
Preserves source IPVia X-Forwarded-For headerYes, nativelyYes
WebSockets, gRPC, HTTP/2YesPasses throughN/A
Typical useWeb apps, APIs, microservicesHigh throughput, non-HTTP, static IPsThird-party firewalls and IDS

The short version: ALB for HTTP, NLB for everything else. Two things push an HTTP workload to an NLB anyway. A client that requires a fixed IP to allowlist, and a need for extreme throughput at minimal latency.

The Classic Load Balancer still exists. Don't start anything new on it.

Structure

Three objects make up a load balancer, and understanding the split makes the rest straightforward:

  • The load balancer owns the DNS name, the subnets, and the security group.
  • Listeners watch a port and protocol, and hold the rules that decide where traffic goes.
  • Target groups hold the actual targets, instances, IPs, or Lambda functions, and own the health check.

Health checks belong to the target group, not the load balancer. Two target groups behind one ALB can check different paths with different thresholds.

# Target group
aws elbv2 create-target-group \
  --name payments-tg \
  --protocol HTTP --port 8080 \
  --vpc-id vpc-0abc123 \
  --target-type ip \
  --health-check-path /healthz \
  --health-check-interval-seconds 15 \
  --healthy-threshold-count 2 \
  --unhealthy-threshold-count 2

# Load balancer across at least two AZs
aws elbv2 create-load-balancer \
  --name payments-alb \
  --type application \
  --subnets subnet-0aaa subnet-0bbb \
  --security-groups sg-0abc123 \
  --scheme internet-facing

An ALB requires subnets in at least two Availability Zones, each with a minimum of 8 free IP addresses. It scales by adding nodes, and it can't scale into a subnet with no room.

Listener rules

This is where ALBs earn their price. One load balancer fronting many services:

aws elbv2 create-rule \
  --listener-arn $LISTENER_ARN \
  --priority 10 \
  --conditions Field=path-pattern,Values='/api/payments/*' \
  --actions Type=forward,TargetGroupArn=$PAYMENTS_TG

Rules evaluate by priority, lowest number first, and the default action catches anything unmatched. Beyond forwarding, rules can redirect (HTTP to HTTPS in one rule, no application code), return a fixed response, or authenticate against Cognito or an OIDC provider before traffic ever reaches your service.

Weighted target groups make canary deploys straightforward. Send 5% of traffic to the new version, watch the metrics, then shift.

Health checks

Most load balancer incidents are really health check misconfigurations.

Interval:            15s     how often each target is checked
Timeout:             5s      must be less than the interval
Healthy threshold:   2       consecutive passes to return to service
Unhealthy threshold: 2       consecutive failures to remove
Success codes:       200

Time to detection is interval × unhealthy threshold, so the settings above take 30 seconds to pull a dead target. That's a real availability decision, not a default to leave alone.

Point the check at an endpoint that reflects whether the instance can actually serve traffic, but not one that checks every downstream dependency. A health check that fails when the database is slow will remove every target simultaneously and turn a degraded system into a total outage. Check that the process is alive and can serve; let dependency failures surface as errors.

/healthz should not require authentication, should not be logged at high volume, and should be cheap.

Draining connections

When a target deregisters, a deploy, a scale-in, the load balancer stops sending it new connections but lets existing ones finish. That window is the deregistration delay, 300 seconds by default:

aws elbv2 modify-target-group-attributes \
  --target-group-arn $TG_ARN \
  --attributes Key=deregistration_delay.timeout_seconds,Value=30

300 seconds makes deploys crawl. Set it slightly above your longest normal request; 30 seconds suits most APIs. Set it too low and you cut off in-flight requests, which shows up to users as failed requests during every deploy.

TLS

Terminate TLS at the load balancer with an ACM certificate. ACM certificates are free and renew automatically:

aws elbv2 create-listener \
  --load-balancer-arn $ALB_ARN \
  --protocol HTTPS --port 443 \
  --certificates CertificateArn=$CERT_ARN \
  --ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
  --default-actions Type=forward,TargetGroupArn=$TG_ARN

Choose the security policy deliberately. The default on older load balancers still permits TLS 1.0. A modern policy that requires TLS 1.2 or better is what you want unless you have a specific legacy client.

Redirect HTTP to HTTPS at the listener rather than in application code:

aws elbv2 create-listener \
  --load-balancer-arn $ALB_ARN --protocol HTTP --port 80 \
  --default-actions '[{"Type":"redirect","RedirectConfig":{"Protocol":"HTTPS","Port":"443","StatusCode":"HTTP_301"}}]'

Debugging 5xx errors

The status code tells you which side failed, which narrows it immediately:

CodeMeaningUsual cause
502 Bad GatewayTarget responded, but malformedApp crashed mid-response, or the target speaks HTTPS while the target group is set to HTTP
503 Service UnavailableNo healthy targetsAll targets failing health checks, or the group is empty
504 Gateway TimeoutTarget too slowResponse exceeded the idle timeout (60s default)
460Client disconnected firstClient gave up: often a downstream timeout shorter than yours
500 from the ELB itselfLoad balancer errorRare; check HTTPCode_ELB_5XX_Count in CloudWatch and the AWS health dashboard

Start with the target group, always:

aws elbv2 describe-target-health --target-group-arn $TG_ARN
{
  "TargetHealthDescriptions": [
    {
      "Target": { "Id": "10.0.1.42", "Port": 8080 },
      "TargetHealth": {
        "State": "unhealthy",
        "Reason": "Target.Timeout",
        "Description": "Request timed out"
      }
    }
  ]
}

The Reason field is precise and worth reading carefully:

  • Target.Timeout. The app didn't respond in time, or a security group is blocking the health check
  • Target.FailedHealthChecks. It responded with the wrong status code
  • Target.ResponseCodeMismatch. A 302 where a 200 was expected, often an auth redirect on /healthz
  • Elb.RegistrationInProgress, just wait
  • Target.NotInUse. The target is in an AZ the load balancer isn't enabled in

The security group case deserves special mention because it produces a healthy-looking application that never receives traffic: the target's security group must allow inbound from the load balancer's security group on the target port. Referencing the load balancer's security group by ID rather than a CIDR is the correct form.

Sticky sessions

Available, but treat them as a workaround:

aws elbv2 modify-target-group-attributes \
  --target-group-arn $TG_ARN \
  --attributes Key=stickiness.enabled,Value=true \
                Key=stickiness.type,Value=lb_cookie \
                Key=stickiness.lb_cookie.duration_seconds,Value=3600

Stickiness produces uneven load, breaks graceful deploys, and means one lost target loses real user state. Externalising session state to Redis or DynamoDB is nearly always the better fix.

Access logs

Off by default. Turn them on before you need them:

aws elbv2 modify-load-balancer-attributes \
  --load-balancer-arn $ALB_ARN \
  --attributes Key=access_logs.s3.enabled,Value=true \
                Key=access_logs.s3.bucket,Value=my-alb-logs

Logs land in S3 and query cleanly from Athena, which is how you answer "which client caused that spike" after the fact. Pair them with a lifecycle policy so the bucket doesn't grow forever.

Quick reference

TaskCommand
Target healthaws elbv2 describe-target-health --target-group-arn <arn>
List load balancersaws elbv2 describe-load-balancers
Listener rulesaws elbv2 describe-rules --listener-arn <arn>
Register a targetaws elbv2 register-targets --target-group-arn <arn> --targets Id=<id>
Drain a targetaws elbv2 deregister-targets --target-group-arn <arn> --targets Id=<id>
Group attributesaws elbv2 describe-target-group-attributes --target-group-arn <arn>

Key takeaways

  • ALB for HTTP, NLB for everything else: or when you need a static IP to hand a client for allowlisting.
  • Health checks live on the target group and detection takes interval × unhealthy threshold. Treat those numbers as an availability decision.
  • Never health-check downstream dependencies. It converts a degraded database into a full outage by removing every target at once.
  • 502 means the target replied badly, 503 means nothing was healthy, 504 means too slow. The code tells you where to look.
  • The target's security group must allow the load balancer's security group on the target port. Reference it by ID, not CIDR.
  • Lower the 300-second deregistration delay to just above your longest request, or every deploy will crawl.

Check your understanding

Exercise

A sudden 503

An ALB starts returning 503 for all requests. The application instances are running and healthy when curled directly from a bastion host. What is the most likely cause?

Exercise

Designing a health check

Should the /healthz endpoint verify that the database is reachable?


Frequently Asked Questions

What is the difference between an ALB and an NLB?

An ALB works at layer 7 and understands HTTP, so it can route on path, host, header, method or query string, terminate TLS, redirect, and authenticate against an identity provider. An NLB works at layer 4 and only sees TCP, UDP and TLS, so it routes on port alone, but it handles far higher throughput at lower latency, supports a static Elastic IP per Availability Zone, and preserves the client source IP natively rather than through an X-Forwarded-For header. Use an ALB for HTTP applications and an NLB for non-HTTP protocols, extreme throughput, or when a client needs a fixed IP address to allowlist.

Why is my ALB returning 502 Bad Gateway?

The load balancer reached a target but could not parse the reply. The two most common causes are the application crashing or closing the connection partway through a response, and a protocol mismatch. The target group set to HTTP while the application only speaks HTTPS on that port, or the reverse. Also check that your application's keep-alive timeout is longer than the ALB's idle timeout; if the target closes an idle connection the ALB still considers open, the next request on it produces a 502.

Why do my targets show as unhealthy when the application works fine?

Run aws elbv2 describe-target-health and read the Reason field before anything else. Target.Timeout usually means the target's security group does not allow inbound traffic from the load balancer's security group on the health check port. Reference the load balancer's security group by ID rather than a CIDR range. Target.ResponseCodeMismatch means the endpoint returned something outside the success codes, often a 302 redirect because the health check path sits behind authentication.

How do I make deploys faster without dropping requests?

Lower the deregistration delay from its 300-second default to just above your longest normal request: 30 seconds works for most APIs. That is how long the load balancer waits for in-flight requests to finish after a target starts draining. Setting it too low cuts off active requests and users see errors on every deploy; leaving it at 300 means every rolling deploy waits five minutes per batch for no benefit.

Should I use sticky sessions?

Only as a temporary measure. Stickiness distributes load unevenly, complicates rolling deploys because draining a target strands its sessions, and means losing one target loses real user state. The durable fix is to keep session state outside the application, in ElastiCache for Redis or DynamoDB, so any target can serve any request. If you must enable stickiness, prefer the load balancer cookie and keep the duration short.

Do I need one load balancer per service?

No, and you generally should not. A single ALB can front many services using listener rules that route on host or path, which saves both the hourly charge and a great deal of DNS and certificate management. Separate load balancers make sense when services need genuinely different configurations. Different security policies, WAF rules, or internal versus internet-facing schemes, or when you want hard blast-radius isolation between them.

Continue Learning

Explore Related Topics

When it breaks

Related Resources