A 502 from an ALB means the load balancer reached your target and could not parse or complete the response. That is different from a 503, which means no healthy target existed, and a 504, which means the target did not answer in time.
| Code | Meaning |
|---|---|
| 502 | Target answered with something unusable, or closed the connection |
| 503 | No healthy targets registered |
| 504 | Target did not respond within the idle timeout |
Turn on access logs first
Without them you are guessing.
aws elbv2 modify-load-balancer-attributes \
--load-balancer-arn <arn> \
--attributes Key=access_logs.s3.enabled,Value=true \
Key=access_logs.s3.bucket,Value=acme-alb-logs
Then read them:
aws s3 cp s3://acme-alb-logs/AWSLogs/.../2026/09/21/ - --recursive | zcat | awk '$9==502'
Field 9 is elb_status_code, field 10 is target_status_code. The distinction is the whole diagnosis:
502withtarget_status_code=-→ the target never returned a usable response502withtarget_status_code=502→ your application genuinely returned 502
The second means look at your app. The first means look at the connection.
The keep-alive mismatch, which causes most intermittent 502s
The ALB pools connections to targets. If the target closes a pooled connection just as the ALB sends a request onto it, the request dies and the ALB returns 502.
The rule: the target's keep-alive timeout must be longer than the ALB's idle timeout.
aws elbv2 describe-load-balancer-attributes --load-balancer-arn <arn> \
--query "Attributes[?Key=='idle_timeout.timeout_seconds']"
Default is 60 seconds. Common application defaults are shorter, which is exactly backwards:
| Server | Default | Set to |
|---|---|---|
| nginx | keepalive_timeout 75s | fine |
| Node.js | server.keepAliveTimeout 5s | 65000 ms |
Go net/http | no timeout by default | fine |
| Gunicorn | --keep-alive 2 | 65 |
| Spring Boot (Tomcat) | 60s | 65s |
Node.js is the classic:
const server = app.listen(8080)
server.keepAliveTimeout = 65000
server.headersTimeout = 66000
headersTimeout must exceed keepAliveTimeout or Node closes the socket anyway.
This produces 502s at a low, steady rate that correlate with nothing, which is what makes them hard to chase. If your 502 rate is a fraction of a percent and the application logs show nothing, check this before anything else.
Health checks and deregistration
A target being removed mid-request returns 502 or a reset. Deregistration delay lets in-flight requests finish:
aws elbv2 modify-target-group-attributes \
--target-group-arn <arn> \
--attributes Key=deregistration_delay.timeout_seconds,Value=60
The delay must exceed your longest request, and your application must handle SIGTERM by draining rather than exiting immediately. A container that exits on the first SIGTERM drops every in-flight request no matter what the delay is set to.
Response problems
The ALB rejects malformed responses:
- Headers over 8 KB. A large
Set-Cookieor a JWT in a header hits this. The limit is not configurable. - Invalid characters in a header. A newline or non-ASCII in a header value.
- Duplicate
Content-Lengthor aContent-Lengththat disagrees with the body. - The target crashed mid-response, which is an OOM kill in many cases.
curl -sv -o /dev/null -m 5 http://10.0.1.42:8080/ 2>&1 | tail -20
Run that from inside the VPC. A clean response there with 502s through the ALB points at headers or timing.
HTTPS to the target
If the target group protocol is HTTPS, the target must actually serve TLS on that port. A plain HTTP server behind an HTTPS target group returns 502 immediately and consistently.
The ALB does not validate the target's certificate, so self-signed is fine. It does require TLS to be there.
aws elbv2 describe-target-groups --target-group-arns <arn> \
--query 'TargetGroups[].{Protocol:Protocol,Port:Port,HealthPath:HealthCheckPath}'
Security groups
aws elbv2 describe-target-health --target-group-arn <arn>
Target.Timeout in the health description means the ALB cannot reach the target at all, which is usually a security group. The target's group must allow inbound on the target port from the ALB's security group, not from a CIDR.
A checklist
- Enable access logs.
elb_status_codeandtarget_status_codeseparate the causes. target_status_code=502→ your application.-→ the connection.- Intermittent and unexplained → keep-alive shorter than the ALB idle timeout. Fix the app.
- Node.js →
keepAliveTimeout = 65000and a largerheadersTimeout. - 502s during deploys → deregistration delay, plus
SIGTERMdraining in the app. - Consistent 502 → check target group protocol matches what the target serves.
- Response headers over 8 KB are rejected and the limit is not configurable.
Target.Timeoutin target health → security group from the ALB's SG.
Frequently Asked Questions
What is the difference between 502, 503 and 504 from an ALB?
502 means the ALB connected to a target and got back something it could not use, including a connection closed mid-response. 503 means there were no healthy targets to send the request to at all. 504 means a target accepted the request and did not respond within the idle timeout. They point at completely different things: 502 at the response or the connection, 503 at target registration and health checks, and 504 at application latency.
Why do I get occasional 502s with nothing in my application logs?
Almost certainly a keep-alive mismatch. The ALB pools connections to targets, and if your application's keep-alive timeout is shorter than the ALB's idle timeout, the target closes a pooled connection at the moment the ALB is placing a request on it. The request fails before your application sees it, which is why the logs are empty. Set the application's keep-alive above the ALB's idle timeout, which defaults to 60 seconds; Node.js is the usual offender with a 5 second default.
Why does Node.js need both keepAliveTimeout and headersTimeout set?
keepAliveTimeout controls how long an idle connection is kept open, and headersTimeout caps how long the server waits for the complete request headers. If headersTimeout is shorter than or equal to keepAliveTimeout, Node can close a connection while the ALB is still sending, which reintroduces the same race you were trying to eliminate. The convention is to set headersTimeout slightly above keepAliveTimeout, for example 65000 and 66000 milliseconds against a 60 second ALB idle timeout.
Why do I see 502s only during deployments?
Targets are being deregistered while requests are still in flight. Set deregistration_delay.timeout_seconds on the target group to longer than your slowest request so the ALB stops sending new traffic and waits for existing requests to finish. That is only half of it: the application must also handle SIGTERM by refusing new work and finishing what it has, rather than exiting immediately. A container that dies on the first signal drops in-flight requests regardless of the delay.
Does the ALB validate my target's TLS certificate?
No. If the target group protocol is HTTPS, the ALB requires the target to speak TLS but does not verify the certificate, so self-signed certificates are fine for the load balancer to target hop. What does fail, immediately and consistently, is a target group configured for HTTPS in front of a target serving plain HTTP. Check that the target group's protocol and port match what the application actually listens with before investigating anything more subtle.