AWS Application Load Balancer

By Vishvesh Patel · DevOps EngineerUpdated September 16, 2026

Layer 7 load balancing on AWS. Listener rule precedence, target group health checks, the 502 and 503 errors you will meet, and ALB vs NLB.

An ALB operates at layer 7, so it understands HTTP. That is what lets it route on path, host, header and method, terminate TLS, and check the health of targets with a real HTTP request rather than a TCP handshake.

The structure is always the same: a listener accepts connections on a port, rules on that listener decide where each request goes, and each rule forwards to a target group containing the instances, IPs or Lambda functions that serve it.

Creating one

An ALB needs at least two subnets in different availability zones. This is not optional, and it is the first error people hit:

aws elbv2 create-load-balancer --name web-alb \
  --subnets subnet-0a1b2c3d subnet-4e5f6a7b \
  --security-groups sg-0123456789abcdef0 \
  --scheme internet-facing --type application
aws elbv2 create-target-group --name web-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 3

aws elbv2 create-listener --load-balancer-arn "$ALB_ARN" \
  --protocol HTTPS --port 443 \
  --certificates CertificateArn="$CERT_ARN" \
  --default-actions Type=forward,TargetGroupArn="$TG_ARN"

Note the port mismatch: the listener is on 443 and the target group on 8080. The ALB terminates TLS and speaks plain HTTP to your targets, which is the normal arrangement.

Health checks decide everything

A target that fails its health check receives no traffic. That is the whole point, and it is also why a bad health check takes a service down more reliably than a bad deployment.

aws elbv2 describe-target-health --target-group-arn "$TG_ARN" \
  --query 'TargetHealthDescriptions[].[Target.Id,TargetHealth.State,TargetHealth.Reason]' \
  --output table
------------------------------------------------------
|  10.0.1.42  |  unhealthy  |  Target.ResponseCodeMismatch  |
|  10.0.2.17  |  healthy    |  None                          |
------------------------------------------------------

The Reason field names the cause, and it is the first thing to read:

ReasonCause
Target.ResponseCodeMismatchWrong status (often a redirect to HTTPS, or 404)
Target.TimeoutHealth check path too slow, or nothing listening
Target.FailedHealthChecksConnection refused; check the security group
Elb.RegistrationInProgressNormal, briefly, after registering
Target.NotInUseTarget not in a subnet the ALB covers

Two things worth getting right. Give the check a dedicated path that does not touch the database, a /healthz returning 200 unconditionally, because a health check that fails when a dependency is slow removes every target at once and turns a degraded dependency into a total outage. And match the expected codes if your app redirects: a health check hitting / that receives a 301 to HTTPS is marked unhealthy unless you set --matcher HttpCode=200,301.

The deregistration delay also matters during deploys:

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

The default is 300 seconds, so a rolling deployment waits five minutes per target. Set it slightly above your longest request.

Listener rules are evaluated in priority order

aws elbv2 create-rule --listener-arn "$LISTENER_ARN" --priority 10 \
  --conditions '[{"Field":"path-pattern","Values":["/api/*"]}]' \
  --actions Type=forward,TargetGroupArn="$API_TG_ARN"

aws elbv2 create-rule --listener-arn "$LISTENER_ARN" --priority 20 \
  --conditions '[{"Field":"host-header","Values":["admin.example.com"]}]' \
  --actions Type=forward,TargetGroupArn="$ADMIN_TG_ARN"

Lowest priority number wins, and evaluation stops at the first match. A broad rule at priority 5 shadows everything below it. The usual cause of "my new rule is being ignored". List them in order before debugging anything else:

aws elbv2 describe-rules --listener-arn "$LISTENER_ARN" \
  --query 'Rules[].[Priority,Conditions[0].Values[0],Actions[0].Type]' --output table

Rules can do more than forward. Redirect and fixed responses avoid a round trip to your servers entirely:

# HTTP → HTTPS on the port 80 listener
--default-actions '[{"Type":"redirect","RedirectConfig":{"Protocol":"HTTPS","Port":"443","StatusCode":"HTTP_301"}}]'

# Block a path at the edge
--actions '[{"Type":"fixed-response","FixedResponseConfig":{"StatusCode":"403","ContentType":"text/plain","MessageBody":"Forbidden"}}]'

Reading the errors

502 Bad Gateway: the ALB reached your target and could not understand the response. Almost always the application: it closed the connection, crashed mid-request, returned malformed headers, or its own keep-alive timeout is shorter than the ALB's 60-second idle timeout, so the target closes a connection the ALB is about to reuse. Setting your server's keep-alive above the ALB idle timeout fixes a surprising number of intermittent 502s.

503 Service Unavailable: no healthy targets. Not an application error at all; go straight to describe-target-health.

504 Gateway Timeout: the target did not respond within the idle timeout. Either the request is genuinely slow or the timeout is too low:

aws elbv2 modify-load-balancer-attributes --load-balancer-arn "$ALB_ARN" \
  --attributes Key=idle_timeout.timeout_seconds,Value=120

Connection refused with healthy targets: the target's security group must allow the ALB's security group on the target port. Referencing the ALB's security group as the source, rather than a CIDR, is the correct pattern.

Sticky sessions

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=86400

Stickiness pins a client to one target with a cookie. It works, and it is usually a workaround for state held in memory that should be in ElastiCache or a database. The costs are real: uneven load distribution, and a target's removal dropping sessions. Treat it as a stopgap while making the application stateless.

Client IP and access logs

The ALB terminates the connection, so your application sees the ALB's IP. The real client is in X-Forwarded-For, and X-Forwarded-Proto tells you whether the original request was HTTPS, which matters for generating correct absolute URLs and for redirect loops.

Enable access logs 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

They record target_processing_time separately from request_processing_time, which is how you establish whether slowness is yours or the balancer's. Query them with Athena.

ALB or NLB

ALBNLB
Layer7 (HTTP)4 (TCP/UDP)
Routes onPath, host, header, methodPort only
Preserves client IPNo (X-Forwarded-For)Yes
Static IPNoYes, one per AZ
Non-HTTP protocolsNoYes
WebSocketsYesYes

Use an ALB for HTTP and HTTPS. Use an NLB for other protocols, when you need a static IP, or for extreme throughput with minimal latency.

Quick reference

TaskCommand
Create an ALBaws elbv2 create-load-balancer --type application
Create a target groupaws elbv2 create-target-group --health-check-path /healthz
Check target healthaws elbv2 describe-target-health --target-group-arn "$TG"
List rules in orderaws elbv2 describe-rules --listener-arn "$L"
Register a targetaws elbv2 register-targets --targets Id=…
Change idle timeout--attributes Key=idle_timeout.timeout_seconds,Value=120
Shorten drain timeKey=deregistration_delay.timeout_seconds,Value=30
Enable access logsKey=access_logs.s3.enabled,Value=true

Key takeaways

  • An ALB needs subnets in at least two availability zones.
  • describe-target-health and its Reason field is the first command for any ALB problem.
  • Health checks must not depend on the database, or a slow dependency becomes a full outage.
  • Rules evaluate lowest priority first and stop at the first match: a broad rule shadows the rest.
  • 503 means no healthy targets; 502 usually means your application closed the connection.
  • Set server keep-alive above the ALB's 60-second idle timeout to stop intermittent 502s.
  • Lower the 300-second deregistration delay or every deploy crawls.
  • The client IP is in X-Forwarded-For, and the ALB's IP is what your app sees.

Check your understanding

Exercise

Every request returns 503

An ALB returns 503 Service Unavailable for all requests. The application runs fine when tested directly on the instances. Where should you look?

Exercise

A rule that is never reached

A new rule at priority 50 routes /api/v2/* to a new target group, but those requests keep going to the old one. An existing rule at priority 10 matches /api/*. What is happening?


Frequently Asked Questions

What is the difference between an ALB and an NLB?

An ALB works at layer 7, so it parses HTTP and can route on path, host header, HTTP header and method, terminate TLS, and perform HTTP health checks. An NLB works at layer 4, forwarding TCP and UDP without inspecting content, which makes it faster, gives it a static IP per availability zone, lets it carry non-HTTP protocols, and means it preserves the client's source IP. Use an ALB for web traffic, and an NLB when you need a static IP, a non-HTTP protocol, or extreme throughput.

Why is my ALB returning 502 Bad Gateway?

The ALB reached your target and could not use the response, so the cause is nearly always on the target side: the connection was closed early, the application crashed mid-request, or the response headers were malformed. The subtle and common case is a keep-alive mismatch, if your server's keep-alive timeout is shorter than the ALB's 60-second idle timeout, the target closes a connection the ALB is about to reuse, producing intermittent 502s under no particular load. Set the server's keep-alive above the ALB's idle timeout.

Why is my ALB target unhealthy when the app works fine?

Read the Reason in describe-target-health. Target.ResponseCodeMismatch usually means the health check path returns a redirect to HTTPS or a 404. Fix the path or widen --matcher to accept it. Target.FailedHealthChecks typically means the target's security group does not allow the ALB's security group on the target port. Target.Timeout means nothing is listening or the path is too slow. Testing the app from your laptop does not exercise any of those.

What should an ALB health check endpoint do?

Return 200 quickly and check nothing external. A health check that queries the database seems more thorough but is actively dangerous: when the database slows, every target fails simultaneously and the ALB removes all of them, converting a degraded dependency into a complete outage. Use a /healthz that confirms the process is serving requests, and monitor dependencies separately with real alarms. If you need a dependency-aware check, keep it on a different path used for readiness gating rather than for load balancer membership.

How do I get the real client IP behind an ALB?

Read the X-Forwarded-For header. The ALB terminates the connection, so the source IP your application sees is the load balancer's. Take the value your ALB appended rather than trusting the whole chain, since clients can send their own. X-Forwarded-Proto is equally important: it tells you whether the original request was HTTPS, which matters for building absolute URLs and for avoiding redirect loops when your app decides it needs to force HTTPS.

Why are my deployments slow behind an ALB?

The deregistration delay, which defaults to 300 seconds. When a target is deregistered the ALB stops sending it new requests but waits that long for in-flight ones to complete before considering it drained, so a rolling deployment pauses five minutes per target. Set deregistration_delay.timeout_seconds to a little above your longest expected request, commonly 30 seconds, and rolling deployments get dramatically faster without cutting anything off mid-request.

Continue Learning

Explore Related Topics

When it breaks

Related Resources