Fix 502 Bad Gateway on AWS API Gateway and Load Balancers
Explore with AI
502 Bad Gateway On AWS a 502 means the proxy in front of your code, usually API Gateway or an Application Load Balancer, got an answer from the backend it couldn't use. With API Gateway and a Lambda proxy integration, the usual cause is a function that throws or returns something other than the statusCode, headers and string body shape. With an ALB, it is usually a target that closes keep-alive connections before the load balancer's 60 second idle timeout, or a target that is deregistered while it still has requests in flight.
Ongoing incident? Our founding team can help you get your AWS app back up.You call your API or open your site and get 502 Bad Gateway. Your code may not have logged an error at all. That’s because AWS writes the 502 itself: something sits between the client and your code, and it’s telling you the reply it got from your code was broken, missing or cut off.
On AWS that something is almost always one of two services. One is Amazon API Gateway, usually in front of a Lambda function. The other is an Application Load Balancer (ALB) in front of EC2 instances, ECS tasks or Lambda. Each has its own list of causes and its own logs that confirm them. Start by working out which one sent the response.
What 502 Bad Gateway means on AWS
A gateway status code describes the hop between the proxy and your backend. A 502 means the proxy reached your backend, or tried to, and got nothing usable back. A 504 is a different problem: the proxy gave up waiting.
The first question I ask about any 502 is which hop wrote it. The answer tells you which logs to open.
graph TB A[Client gets 502 Bad Gateway] --> B{Which service fronts the app?} B -->|API Gateway| C{Log says malformed or failed?} C -->|malformed| D[Return statusCode and string body] C -->|failed| E[Check Lambda errors and permissions] B -->|ALB| F{HTTPCode_ELB_502_Count has data?} F -->|yes| G[Read access logs for the cause] F -->|no| H[Your app returned the 502 itself] style D fill:#d1fae5,stroke:#6ee7b7,color:#065f46 style G fill:#d1fae5,stroke:#6ee7b7,color:#065f46
If the URL looks like https://abc123.execute-api.us-east-1.amazonaws.com/... or is a custom domain mapped to an API, read the API Gateway section. If DNS for your domain points at a load balancer (*.elb.amazonaws.com), read the ALB section.
Fixing API Gateway 502s from a Lambda proxy integration
With a Lambda proxy integration, API Gateway passes the raw request to your function and expects a fixed response shape back. The API Gateway docs say that if the function’s output is in any other format, API Gateway returns a 502 Bad Gateway error response. The shape is:
{
"isBase64Encoded": false,
"statusCode": 200,
"headers": { "Content-Type": "application/json" },
"multiValueHeaders": { "Set-Cookie": ["a=1", "b=2"] },
"body": "a string"
}
headers and multiValueHeaders are optional. body must be a string.
Confirm it in the execution logs
Turn on CloudWatch execution logging for the stage (setup steps), call the API again and open the log group. The AWS Knowledge Center article on this error shows the two patterns to look for:
Execution failed due to configuration error:
Method completed with status: 502
Lambda execution failed with status 200 due to customer function error: [Errno 13] Permission denied: '/var/task/lambda_function.py'. Lambda request id: ...
Method completed with status: 502
The first means the function returned the wrong shape. The second means the function itself failed. You can also run the method from the CLI, which returns the same log inline:
aws apigateway test-invoke-method \
--rest-api-id abc123 \
--resource-id xyz789 \
--http-method GET \
--query log --output text
Cause 1: the response has the wrong shape
How to tell: the log says the execution failed with a configuration error, and the Lambda’s own logs show it finished without an exception.
The usual mistakes are a body that is an object, a missing statusCode, or a handler that returns nothing at all.
- Return an object with a numeric
statusCodeand a stringbody. In Node.js, stringify JSON yourself:export const handler = async (event) => { const data = { ok: true }; return { statusCode: 200, headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), isBase64Encoded: false, }; }; - In Python, the same rule applies:
import json def handler(event, context): return { "statusCode": 200, "headers": {"Content-Type": "application/json"}, "body": json.dumps({"ok": True}), } - Make sure every code path returns. An
asynchandler that fires a promise and doesn’tawaitit can finish withundefined, which is not a valid proxy response. - If you return binary data, base64-encode the body, set
isBase64Encodedtotrueand add the media type under the API’s binary media types, as the docs describe.
Cause 2: the function throws to signal an HTTP error
How to tell: you see 502s only on the paths where your code handles an error, such as not found or bad input. The execution log shows a customer function error.
This is a common version of the question. Code that calls context.fail(...), callback(new Error(...)) or throw to send a 404 makes the invocation fail, and API Gateway turns a failed invocation into a 502. The docs spell out the fix: for a client-side error, return the status code as a normal result.
- Replace the throw with a returned response:
if (!item) { return { statusCode: 404, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ error: "not found" }), }; } - Wrap the handler body in
try/catchand return a 500 response from thecatch, logging the error first so it still reaches CloudWatch. - Keep
throwfor errors you actually want to surface as a gateway failure.
Cause 3: the function times out or can’t run
How to tell: the Lambda logs for the request end without your handler’s output, show a timeout, or show an import or permission error. The 502 often appears on cold starts or on slow downstream calls.
The Lambda timeout docs set the default timeout at 3 seconds, adjustable up to 900 seconds. A function that loads a framework on a cold start can run past 3 seconds on its first call.
- Raise the timeout to something your slowest real request fits inside:
aws lambda update-function-configuration \ --function-name my-function \ --timeout 30 - Check that the handler name in the function configuration matches the file and export, for example
index.handlerforexport const handlerinindex.mjs. - Check that the function’s resource policy lets API Gateway invoke it. The Knowledge Center article lists this as a cause. Add the permission if it’s missing:
aws lambda add-permission \ --function-name my-function \ --statement-id apigateway-invoke \ --action lambda:InvokeFunction \ --principal apigateway.amazonaws.com \ --source-arn "arn:aws:execute-api:us-east-1:123456789012:abc123/*/*/*" - For a
Permission deniedon files in/var/task, rebuild the deployment package with readable file permissions and redeploy.
Fixing ALB 502s: find out whether the load balancer or the target sent it
An ALB either forwards a target’s response or generates its own. The ALB troubleshooting guide lists every reason the load balancer itself sends a 502. The Knowledge Center article on ALB 502s gives the first split:
- Data in
HTTPCode_ELB_502_Countmeans the load balancer generated the 502. - Data in
HTTPCode_Target_5XX_Countmeans your target returned it, and the ALB passed it on. Debug your app.
aws cloudwatch get-metric-statistics \
--namespace AWS/ApplicationELB \
--metric-name HTTPCode_ELB_502_Count \
--dimensions Name=LoadBalancer,Value=app/my-alb/50dc6c495c0c9188 \
--start-time 2026-10-18T00:00:00Z --end-time 2026-10-18T12:00:00Z \
--period 300 --statistics Sum
Then turn on access logs. In each line, an elb_status_code of 502 with a target_status_code of - means the ALB generated it. Both fields at 502 means the target did. For Lambda targets, the error_reason field names the failure.
# ALB access logs are gzipped files in S3. Fields 9 and 10 are elb_status_code and target_status_code.
aws s3 cp s3://my-alb-logs/AWSLogs/123456789012/elasticloadbalancing/us-east-1/2026/10/18/ ./logs --recursive
zcat logs/*.gz | awk '$9 == 502 { print $2, $5, $6, $7, $8, $10, $13 }' | head
Cause 4: the target closes keep-alive connections too early
How to tell: a small, steady trickle of 502s on healthy targets, often on the first request after a quiet spell. The access log shows the request reached a target and the target status is -.
The ALB keeps connections to your targets open and reuses them. The load balancer attributes docs set the idle timeout at 60 seconds by default. If your server closes an idle connection first, the ALB can send a request down a socket that is already closing, and the client gets a 502. AWS recommends setting your application’s idle timeout higher than the load balancer’s.
- Read the ALB’s idle timeout:
aws elbv2 describe-load-balancer-attributes \ --load-balancer-arn "$ALB_ARN" \ --query "Attributes[?Key=='idle_timeout.timeout_seconds']" - Set your server’s keep-alive timeout above it. In Node.js that is
server.keepAliveTimeout. A common practice is to also setheadersTimeouta little higher thankeepAliveTimeout:const server = app.listen(8080); server.keepAliveTimeout = 65000; // above the ALB's 60 s idle timeout server.headersTimeout = 66000; // common practice: a little above keepAliveTimeout - In NGINX on the target:
http { keepalive_timeout 75s; } - In Gunicorn, pass
--keep-alive 75. For other servers, find the keep-alive or idle timeout setting and raise it the same way.
Cause 5: 502s during deploys and scale-in
How to tell: the 502s cluster around ECS deployments, Auto Scaling scale-in or instance replacement. CloudTrail shows DeregisterTargets calls at the same time.
When a target is deregistered it goes into draining. The target group attributes docs say the default deregistration delay is 300 seconds. If the delay runs out while a request is still in flight, or the task stops before the delay ends, the client gets a 5xx. Teams often shorten this delay to make deploys faster, then see 502s on rollouts.
- Check the current delay:
aws elbv2 describe-target-group-attributes \ --target-group-arn "$TG_ARN" \ --query "Attributes[?Key=='deregistration_delay.timeout_seconds']" - Set it longer than your slowest request:
aws elbv2 modify-target-group-attributes \ --target-group-arn "$TG_ARN" \ --attributes "Key=deregistration_delay.timeout_seconds,Value=60" - Make the app finish in-flight requests when it gets its stop signal, and give it enough time to stop before the container is killed.
Cause 6: the target refuses the connection
How to tell: TargetConnectionErrorCount has data points, and in the access log request_processing_time, target_processing_time and response_processing_time are all -1. That means the ALB never delivered the request.
The ALB got a TCP RST, or an ICMP host unreachable, when it tried to connect. Nothing is listening on the target port, or the network blocks the path.
- Confirm the app listens on the target group’s port on all interfaces (
0.0.0.0), and not only on127.0.0.1. - Check that the target’s security group allows the target port from the load balancer’s security group.
- Check that network ACLs on both sets of subnets allow the target port and the ephemeral ports 1024-65535.
- Check target health. The resource map in the console shows the failure reason for each unhealthy target:
aws elbv2 describe-target-health --target-group-arn "$TG_ARN"
Cause 7: a Lambda target fails or replies too big
How to tell: the target is a Lambda function and the access log’s error_reason is set. LambdaResponseTooLarge means the body went over 1 MB. LambdaUnhandled means the function errored or didn’t respond before its timeout. Both also show up in the LambdaUserError metric. Throttling shows up in the function’s Throttles metric.
- For large responses, write the payload to S3 and return a download URL.
- For
LambdaUnhandled, read the function’s logs, fix the error, and raise the timeout if the work needs it. - For throttling, retry with backoff on the client and request a higher concurrency quota.
Less common ALB causes
- TLS handshake errors on HTTPS target groups: the target’s ciphers or certificate don’t match what the ALB offers. Capture packets with
sudo tcpdump -i any -w capture.pcapon the target and check the handshake. - Malformed responses: invalid HTTP headers, or a response header block over 32 K.
- HTTP/2 or gRPC targets that end a stream with
RST_STREAM: the ALB returns a 502 by default. The target group attributerouting.http2.rst_stream_error_forwarding.enabledpasses the target’s error code through.
Keeping 502 Bad Gateway from coming back
- Alarm on the load balancer’s own 502s. An alarm on
HTTPCode_ELB_502_Countcatches the keep-alive and deploy cases, which never show up in your app’s error logs:aws cloudwatch put-metric-alarm \ --alarm-name alb-elb-502 \ --namespace AWS/ApplicationELB \ --metric-name HTTPCode_ELB_502_Count \ --dimensions Name=LoadBalancer,Value=app/my-alb/50dc6c495c0c9188 \ --statistic Sum --period 300 --evaluation-periods 1 \ --threshold 10 --comparison-operator GreaterThanOrEqualToThreshold \ --alarm-actions arn:aws:sns:us-east-1:123456789012:oncall - Keep ALB access logs on. The
error_reasonand processing-time fields are the fastest way to tell causes apart, and they only exist if logging was on when the error happened. - Keep API Gateway execution logging on for at least one stage, so a malformed response shows up as a log line.
- Test the response contract. A unit test that calls each Lambda handler on its error paths and checks for a numeric
statusCodeand a stringbodycatches cause 1 and cause 2 before deploy. - Write timeouts down in one place. The ALB idle timeout, your server’s keep-alive timeout, the deregistration delay and the Lambda timeout all interact. Put them in the same infrastructure code so a change to one prompts a check of the others.
- Line 502 spikes up with deploys. Most ALB 502 bursts start at a rollout, so a deploy marker on the same chart answers the question quickly.
How Polylane catches AWS 502s
Polylane detects production issues and fixes them by opening pull requests. It connects to AWS, so when a load balancer starts returning 502s, it works out why, and the fix arrives ready for your team’s normal review.
Running on AWS? See how Polylane monitors AWS in production.
Common questions.
What is the difference between a 502 and a 504 on AWS?
A 502 means the proxy got a reply it couldn't use, or the connection broke: a malformed Lambda response, a TCP reset or a target that closed the connection. A 504 means the proxy gave up waiting. On an ALB, a 504 comes from failing to connect within 10 seconds or a target that doesn't respond before the idle timeout.
Why does API Gateway return 502 when my Lambda throws an error?
With a Lambda proxy integration, a thrown error or context.fail makes the invocation fail, and API Gateway turns that into a 502. To send a 404 or 400, return an object with that statusCode and a string body. Keep throw for real failures.
Does the body in a Lambda proxy response have to be a string?
Yes. API Gateway expects body as a string, so call JSON.stringify in Node.js or json.dumps in Python before returning. For binary data, base64-encode it, set isBase64Encoded to true and register the media type on the API.
How do I tell whether my ALB or my app returned the 502?
Check CloudWatch: data in HTTPCode_ELB_502_Count means the load balancer generated it, and data in HTTPCode_Target_5XX_Count means your target did. In the access logs, an elb_status_code of 502 with a target_status_code of - points at the load balancer.
Why do I get 502s from my ALB during ECS deployments?
Old tasks are deregistered and enter draining. If the deregistration delay runs out, or the task stops while a request is still in flight, the client gets a 5xx. The default delay is 300 seconds, so check whether yours was lowered, and make the app finish in-flight requests on shutdown.
What keep-alive timeout should my server use behind an ALB?
Set it higher than the ALB's connection idle timeout, which is 60 seconds by default. In Node.js that means setting server.keepAliveTimeout to a value above the ALB idle timeout, and a common practice is to set headersTimeout a little above that. If you raise the ALB idle timeout later, raise the server's keep-alive timeout with it.
What is the default Lambda timeout, and can it cause a 502?
The default is 3 seconds, adjustable up to 900 seconds. A function that runs past its timeout fails the invocation, so API Gateway or an ALB returns a 502. Raise it with aws lambda update-function-configuration --timeout.
Sources (9)
- Troubleshoot your Application Load Balancers (AWS docs)
- How do I troubleshoot Application Load Balancer HTTP 502 errors? (AWS Knowledge Center)
- How do I resolve HTTP 502 errors from API Gateway REST APIs with Lambda proxy integration? (AWS Knowledge Center)
- Lambda proxy integrations in API Gateway (AWS docs)
- Edit target group attributes for your Application Load Balancer (AWS docs)
- Edit attributes for your Application Load Balancer (AWS docs)
- Configure Lambda function timeout (AWS docs)
- Polylane documentation
- Polylane full content
Boris Tane is the founder of Polylane. He previously founded Baselime, observability for the future of the cloud, which Cloudflare acquired. At Cloudflare he built and led the Workers observability team.
Related
- Fix 403 Forbidden (Access Denied) from Amazon S3 Why Amazon S3 returns 403 Forbidden: Block Public Access, missing s3:ListBucket, object ownership, clock skew, expired presigned URLs and CloudFront. Fix each.
- Fix AuthorizerConfigurationException in API Gateway AuthorizerConfigurationException means API Gateway couldn't call your Lambda authorizer or couldn't read its response. Find the cause and fix it.
- Fix Railway “Application failed to respond” (502) Why Railway returns “Application failed to respond” with a 502, how to tell a wrong host, port or target port from a crash or overload, and how to fix each.
- Vercel 504 Gateway Timeout on Serverless Functions: Causes and Fixes Fix Vercel 504 FUNCTION_INVOCATION_TIMEOUT errors: current duration limits, how to find the slow call, the fix for each cause and how to raise maxDuration.
- How to correlate incidents with deploys using evidence Learn how to correlate incidents with deploys using release markers, metrics, logs and verified GitHub, Vercel, Cloudflare and Kubernetes change events.