AWS ALB 503 "No Healthy Targets": Fix Unhealthy Backends
An ALB returning “503 Service Unavailable — No healthy targets” means the load balancer itself is fine, but it has nowhere to send requests. The three most common causes are a missing security group rule between the ALB and the target, a misconfigured health check path or response code, and no targets registered in the target group at all. This guide walks through each one in order.
Quick answer
Run this first:
aws elbv2 describe-target-health \
--target-group-arn <your-target-group-arn> \
--query 'TargetHealthDescriptions[*].[Target.Id,TargetHealth.State,TargetHealth.Reason,TargetHealth.Description]' \
--output tableWhat you see tells you exactly where to go next:
| Result | Most likely cause | Jump to |
|---|---|---|
| Empty list | Nothing registered in the target group | Step 2 |
unhealthy + Target.FailedHealthChecks | ALB cannot reach the target | Step 3 |
unhealthy + Target.ResponseCodeMismatch | ALB reaches target, wrong HTTP status | Step 4 |
initial | Target just registered, first health check pending | Wait 30–90 seconds |
unused | Target group not attached to a listener rule | Check listener rules |
Simple explanation
An Application Load Balancer sits between your users and your servers. Users send requests to the ALB, the ALB picks a healthy server from its target group, and forwards the request there.
A target group is a list of servers (EC2 instances, IP addresses, or Lambda functions) the ALB can send traffic to. The ALB regularly sends a small test request called a health check to each target. A target that passes is marked healthy and receives traffic. A target that fails is marked unhealthy and removed from the rotation.
When every target is unhealthy, or when no targets are registered at all, the ALB has nowhere to send the request. It returns 503 directly to the user. Your application may not be receiving the request at all.
Think of the ALB as a maître d’ at a restaurant. Before seating a guest, they check whether any tables are ready to serve. If every server called in sick, the maître d’ tells the guest there is nothing available right now. The ALB does the same thing: before forwarding a request, it checks which targets passed their last health check. No healthy targets means 503.
What users usually see
- Browser or API client receives
503 Service Unavailablewith body:No healthy targets are available to service this request. - AWS Console shows target group targets in Unhealthy status with reason codes like
Health checks failedorResponse code mismatch - A deployment completes successfully but traffic fails immediately after the new target group goes active
- Application was working, then a security group change or code deployment broke it
- New environment works in testing but fails in production because the security group setup differs
ALB 503 vs application 503
A 503 does not always mean the same thing. Knowing the source changes the fix entirely.
| ALB-generated 503 | Application-generated 503 | |
|---|---|---|
| Who sends it | The ALB itself | Your application code |
| Body text | No healthy targets are available to service this request. | Your app’s error message |
| Target health | All targets unhealthy or target group empty | Targets are healthy from the ALB’s perspective |
| Root cause | Network or config problem between ALB and target | Application logic error, overload, or dependency failure |
| Where to start | describe-target-health | Application logs, APM, CloudWatch metrics |
If the ALB successfully forwards requests to a target, that target appears healthy regardless of whether the app returns 500s or 503s under real traffic. A health-check-passing target returning 503 to users is an application-level problem, not a load balancer problem.
ALB vs NLB troubleshooting differences
Most of this guide applies to both ALB and NLB, but there are important differences in how security groups and health checks work.
| ALB | NLB | |
|---|---|---|
| Security groups | Always has a security group | Optional (NLBs support security groups as of late 2023, but older NLBs may not have one) |
| Health check source | ALB node IPs covered by the ALB security group | NLB node IPs from the NLB’s subnets |
| Health check protocols | HTTP / HTTPS / gRPC | HTTP / HTTPS / TCP / TCP_UDP |
| Source IP seen by target | ALB node IP (client IP in X-Forwarded-For header) | Client IP for TCP, NLB node IP for TLS |
| No-healthy-targets behaviour | ALB returns 503 to the client | NLB drops the connection; clients see connection refused |
For NLBs without a security group: Health check traffic originates from the NLB’s node IPs, which come from the NLB’s subnets. Target security groups must allow inbound TCP on the health check port from those subnet CIDR ranges.
For NLBs with a security group (post-2023 behaviour): You can reference the NLB security group in the target security group inbound rule, the same approach used for ALB. Check whether your NLB has a security group before deciding which rule to add.
See Internal Load Balancers for setup differences when the load balancer is not internet-facing.
How it works
Understanding the request flow makes the debugging steps obvious.
- Listener receives request. The ALB listener on port 443 (or 80) accepts the connection from the client.
- Listener rule selects a target group. The listener evaluates its rules — path, host header, query string — and routes to a matching target group.
- ALB checks target health. Before forwarding, the ALB checks which targets are currently marked healthy based on recent health check results.
- Healthy target selected. The ALB picks a healthy target using the configured routing algorithm and forwards the request.
- No healthy targets → 503. If all targets are unhealthy or the target group is empty, the ALB returns 503 immediately.
Health checks run on a background schedule (default: every 30 seconds). Results are not real-time. A target that just became reachable can take up to 30 seconds per check, and 90 seconds total to cross the 3-check healthy threshold.
Fast triage checklist
Work through these questions in order. Most issues resolve at the third question.
- Are targets registered? Run
describe-target-health. Empty list means nothing is registered. Go to Step 2. - What state are targets in?
initialmeans wait.unusedmeans the target group is not attached to a listener rule.unhealthymeans keep going. - Can the ALB reach the target?
Target.FailedHealthChecksis a connection-level failure. Check security groups and network. Go to Step 3. - Is the app responding correctly?
Target.ResponseCodeMismatchmeans the connection works but the status code is wrong. Check the health check path and matcher. Go to Step 4. - Did the app just start? Wait 60–120 seconds for startup and the first clean health check cycle before investigating further.
- Is deregistration delay creating a timing gap? Old targets draining while new ones are not yet healthy can leave brief gaps in small clusters. See Step 5.
- Is the target group attached to the right listener rule? A target group not referenced by any rule shows all targets as
unused.
Step 1: Check target health status
This is always the first command. The reason codes tell you almost everything.
# Get the target group ARN
aws elbv2 describe-target-groups \
--names my-target-group \
--query 'TargetGroups[0].TargetGroupArn' \
--output text
# Check health of all registered targets
aws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/my-target-group/abc123 \
--query 'TargetHealthDescriptions[*].[Target.Id,TargetHealth.State,TargetHealth.Reason,TargetHealth.Description]' \
--output tableTarget states:
| State | Meaning |
|---|---|
healthy | Passing health checks, receiving traffic |
unhealthy | Registered but failing health checks, not receiving traffic |
initial | Just registered, first health check not yet complete |
draining | Deregistered, waiting for in-flight requests to finish |
unused | Target group not attached to a load balancer listener rule |
Reason codes for unhealthy targets:
| Reason code | What it means | Where to look |
|---|---|---|
Target.FailedHealthChecks | Connection to the target failed entirely | Security groups, NACLs, subnet routing |
Target.ResponseCodeMismatch | Connected, but HTTP status code did not match expected | Health check path, protocol, matcher config |
Target.Timeout | Health check request timed out | App startup delay, network latency, blocked return traffic |
Target.NotInUse | Target group not associated with a load balancer | Listener rule configuration |
Elb.InternalError | ALB internal error | Rare; contact AWS Support if persistent |
Step 2: Confirm targets are actually registered
A common cause of 503 on a brand-new setup is simply that no targets are registered. describe-target-health returning an empty list confirms this.
Manually registered targets — register an EC2 instance directly:
aws elbv2 register-targets \
--target-group-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/my-target-group/abc123 \
--targets Id=i-1234567890abcdef0,Port=80Auto Scaling Group targets — instances register automatically only if the ASG is attached to the target group. Verify the attachment:
aws autoscaling describe-auto-scaling-groups \
--auto-scaling-group-names my-asg \
--query 'AutoScalingGroups[0].TargetGroupARNs'If the target group ARN is not in that list, the Auto Scaling Group is not linked and no instances will register automatically. Attach it:
aws autoscaling attach-load-balancer-target-groups \
--auto-scaling-group-name my-asg \
--target-group-arns arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/my-target-group/abc123Listener rule check — even with healthy registered targets, traffic will not reach them if the target group is not referenced by a listener rule:
aws elbv2 describe-rules \
--listener-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:listener/app/my-alb/abc123/def456Look for your target group ARN in the Actions of one of the rules. If it is absent, no traffic is routed to it regardless of target health.
Step 3: Verify security groups, network path, and reachability
Target.FailedHealthChecks means the ALB cannot establish a connection to the target at all. The most common cause is a missing inbound rule on the target’s security group.
The correct security group setup for ALB:
The ALB sends health check and real traffic from its node IPs. The cleanest way to allow this is to reference the ALB security group ID in the target’s inbound rule, not an IP CIDR range.
# Find the ALB security group
aws elbv2 describe-load-balancers \
--names my-alb \
--query 'LoadBalancers[0].SecurityGroups'
# Allow inbound from the ALB security group on the application port
aws ec2 authorize-security-group-ingress \
--group-id sg-0target123 \
--protocol tcp \
--port 80 \
--source-group sg-0alb456ALBs scale horizontally. When traffic spikes, AWS adds more ALB nodes with new IP addresses. An inbound rule based on IP ranges will work until the ALB scales, then silently drop health checks and requests. Always use the ALB security group ID (sg-xxxx), not a CIDR range.
For NLBs without a security group: Health check traffic comes from the NLB node IPs within the NLB’s subnets. Allow inbound from those subnet CIDRs:
aws ec2 authorize-security-group-ingress \
--group-id sg-0target123 \
--protocol tcp \
--port 80 \
--cidr 10.0.1.0/24When to check Network ACLs and route tables:
Security groups are stateful. If inbound is allowed, return traffic is automatically allowed. Network ACLs are stateless and can block return traffic even when the inbound rule exists. Check NACLs if security groups look correct but the connection still fails.
Route tables matter when the ALB and target are in different subnets or the target is in a private subnet. See Debugging VPC Connectivity for a systematic approach to network-layer failures.
Step 4: Verify health check configuration
Target.ResponseCodeMismatch or Target.Timeout when the connection works means the health check configuration does not match what your application serves.
Check current health check settings:
aws elbv2 describe-target-groups \
--target-group-arns arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/my-target-group/abc123 \
--query 'TargetGroups[0].{Path:HealthCheckPath,Port:HealthCheckPort,Protocol:HealthCheckProtocol,Matcher:Matcher,Interval:HealthCheckIntervalSeconds,Threshold:HealthyThresholdCount}'Wrong path. The default path is /. If your app returns 404 on / because the root is at /api/v1/, health checks fail with ResponseCodeMismatch. Update the path:
aws elbv2 modify-target-group \
--target-group-arn arn:aws:elasticloadbalancing:... \
--health-check-path /healthBest practice: create a dedicated /health endpoint that returns 200 with no business logic and no authentication required. Its only job is to confirm the process is running.
If your health check path requires authentication and returns 401 or 302 (redirect to login page), the ALB marks every target unhealthy. The health endpoint must return 200 without any auth check, every time, regardless of session state. If you cannot add a dedicated endpoint, configure the matcher to accept the redirect code — but fixing the endpoint is cleaner.
Wrong expected response code. The default matcher is 200. If your health endpoint returns 204 No Content, configure the matcher to accept it:
aws elbv2 modify-target-group \
--target-group-arn arn:aws:elasticloadbalancing:... \
--matcher HttpCode=200-299Wrong port. The health check port defaults to traffic-port, the port registered with the target. If your app runs on 8080 but targets were registered on port 80, the health check fires on the wrong port. Specify the correct port explicitly if they differ.
Protocol mismatch. If your application is HTTPS-only and the health check uses HTTP, you get a connection reset or empty response. Match the health check protocol to what the target actually accepts.
Step 5: Startup timing, grace periods, and deployments
A common source of unexpected unhealthy targets is timing: the target is fine, but it was not ready when the health check first fired.
Slow application startup. If your application takes 45 seconds to start and the health check interval is 10 seconds with an unhealthy threshold of 3, the ALB marks the target unhealthy before the app finishes starting. Solutions:
- Add a lightweight
/healthendpoint early in startup that returns 200 once the port is open, before full initialization completes - Increase
HealthCheckIntervalSecondsto give the app more time between checks - Stagger deployments so not all targets restart at the same time
EC2 Auto Scaling health check grace period. When Auto Scaling launches a new EC2 instance, it waits for the health check grace period (default: 300 seconds) before evaluating whether to replace the instance. During this window, Auto Scaling ignores ALB health check results. This grace period is separate from the ALB health check: the ALB starts checking immediately after the target registers, independent of Auto Scaling’s timer.
Instance warmup in scaling policies. For step scaling and target tracking policies, you can configure an instance warmup period. New instances launched during a scale-out event are not counted toward the group’s current capacity until warmup ends. This prevents Auto Scaling from over-provisioning by treating still-starting instances as not yet contributing.
Deregistration delay and rolling deployments. When a target is removed from a target group, the ALB keeps routing to it for the deregistration delay (default: 300 seconds) to let in-flight requests finish. During this window the target shows as draining.
With only 2 targets, if one starts draining for 5 minutes and the replacement takes 2 minutes to become healthy, there is a 3-minute window where you have one draining target and one that is not yet healthy. Plan your minimum healthy capacity around this timing before deploying. Reduce the deregistration delay if deployment speed matters more than completing long-running requests.
Reduce the deregistration delay for environments where deployment speed matters:
aws elbv2 modify-target-group-attributes \
--target-group-arn arn:aws:elasticloadbalancing:... \
--attributes Key=deregistration_delay.timeout_seconds,Value=30Step 6: Logs and observability
describe-target-health shows the current state. Logs and metrics show what happened over time.
CloudWatch metrics. HealthyHostCount and UnHealthyHostCount are available per target group and update every minute. They are the fastest way to see when the problem started and whether it is improving.
aws cloudwatch get-metric-statistics \
--namespace AWS/ApplicationELB \
--metric-name UnHealthyHostCount \
--dimensions Name=TargetGroup,Value=targetgroup/my-target-group/abc123 \
Name=LoadBalancer,Value=app/my-alb/xyz789 \
--start-time 2026-05-13T10:00:00Z \
--end-time 2026-05-13T11:00:00Z \
--period 60 \
--statistics AverageALB access logs. Access logs capture every request the ALB processes and write them to S3. They help confirm whether the ALB is reaching the target: if health check requests appear in the logs with a response, the target was reachable and the issue is the response content. If they do not appear, the connection failed before any response was received.
Access logs are different from your application logs. Access logs record what the ALB saw; your application logs record what happened inside the app after receiving the request. Both are useful but they answer different questions.
ALB access logs are not enabled by default. If you are actively troubleshooting, enable them now on the load balancer attributes in the console or via CLI. They will not show what happened before they were enabled, but they are essential for diagnosing the next incident. The cost is low — you pay only for the S3 storage.
See CloudWatch Logs for setting up log-based alerting once you know what to watch for.
Direct connectivity test. If you have SSH access to the target instance, simulate the health check locally to verify the application is responding:
# From the target instance
curl -v http://localhost:80/health
# From another instance in the same subnet (simulates the ALB path)
curl -v http://10.0.2.50:80/healthThis confirms whether the application is running and responding correctly, independent of the ALB and security group layer.
Common mistakes
- Using ALB IP addresses in the target security group instead of the ALB security group ID. ALBs scale horizontally and their node IPs change. An IP-based rule that works today may break during the next traffic spike. Always reference the ALB security group ID (
sg-xxxx). - Health check path requires authentication. If the health check hits a route that redirects to a login page (302) or returns 401, the ALB marks every target unhealthy. Use a dedicated
/healthendpoint with no auth and no business logic. - Wrong health check path for the application’s URL structure. The default path is
/. If your app returns 404 on/because it is mounted at/api/or/app/, health checks fail with ResponseCodeMismatch immediately. - Target group not attached to a listener rule. Instances can be registered and healthy in a target group, but if no listener rule routes traffic to it, no requests reach them. Targets show as
unused. - Auto Scaling Group not linked to the target group. ASGs do not register instances automatically unless explicitly attached. After creating a target group, verify the ASG’s
TargetGroupARNsincludes it. - Startup delay mismatch causing premature unhealthy marks. The ALB starts health checks immediately after registration. If the app takes 60 seconds to start and health checks run every 10 seconds with a threshold of 3, the target is marked unhealthy before it is ready. Adjust the interval, threshold, or startup sequence.
- Deregistration delay creating a capacity gap during rolling deployments. With a small number of targets, old instances draining for 5 minutes while replacements take 2 minutes to become healthy creates a window with no healthy capacity. Reduce deregistration delay or ensure minimum healthy capacity before starting a rollout.
When to use this guide
This is the right page if:
- The ALB returns 503 with the message “No healthy targets are available”
- The target group shows targets in
unhealthystatus - A new deployment broke traffic behind a load balancer
- Health checks are failing even though the application appears to be running
This is not the right page if:
- You are getting DNS resolution errors or
NXDOMAINresponses. See DNS Resolution Problems in AWS. - The ALB itself is not reachable (connection refused before any HTTP response). See Troubleshooting Network Issues or Debugging VPC Connectivity.
- The application returns 500s or 503s but the ALB shows targets as healthy. That is an application-level problem.
- You are troubleshooting private vs public IP addressing for the load balancer’s own reachability.
Example walkthrough: new deployment returns 503
You deploy a new version of an application. The deployment succeeds but the ALB immediately returns 503 to all users.
Check target health:
aws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/new-tg/xyz789 \
--output tableThree targets, all unhealthy, reason Target.FailedHealthChecks. The ALB cannot connect to them.
Find the ALB security group:
aws elbv2 describe-load-balancers \
--names my-alb \
--query 'LoadBalancers[0].SecurityGroups'
# ["sg-0alb123"]Check the target security group:
aws ec2 describe-instances \
--instance-ids i-target1 \
--query 'Reservations[0].Instances[0].SecurityGroups'
# [{"GroupId": "sg-0newapp456"}]
aws ec2 describe-security-groups \
--group-ids sg-0newapp456 \
--query 'SecurityGroups[0].IpPermissions'
# [] ← emptyThe new deployment used a new security group with no inbound rules. The old security group had the ALB rule, but it was not copied when the new group was created.
Fix:
aws ec2 authorize-security-group-ingress \
--group-id sg-0newapp456 \
--protocol tcp \
--port 80 \
--source-group sg-0alb123Within 30–90 seconds, targets transition to healthy and 503 errors stop.
Frequently asked questions
How long does it take for a target to become healthy after fixing the issue?
It depends on your health check settings. With defaults (30-second interval, threshold of 3 consecutive healthy checks), a target needs at least 90 seconds after the fix before the ALB marks it healthy. During that window the ALB only sends traffic to already-healthy targets.
What is the difference between unhealthy, initial, draining, and unused?
Unhealthy means the target is registered but failing health checks. Initial means it was just registered and has not been checked yet — wait for the first check cycle. Draining means the target is being deregistered and the ALB is waiting for in-flight requests to finish before cutting traffic. Unused means the target group is not attached to any load balancer listener rule.
Can a health check endpoint return 204 or 302?
Yes, but you must configure the ALB to accept those codes. By default the ALB expects HTTP 200. Use --matcher HttpCode=200-299 to accept any 2xx response including 204. For redirects (302), it is better to fix the health endpoint to return 200 directly — a redirect to a login page will also fail the health check.
Why does a deployment fail even though instances are running?
Running is not the same as healthy. A new EC2 instance can be running and reachable but the application might still be initializing. If the ALB health check fires before the app is ready, the target goes unhealthy. The EC2 Auto Scaling health check grace period (default 300 seconds) gives instances time before Auto Scaling acts on them, but the ALB health check starts immediately after registration, independently of that grace period.
When should I troubleshoot DNS instead of target health?
If clients cannot reach the load balancer at all, getting a connection refused or NXDOMAIN error rather than a 503 response, the issue is DNS or routing, not target health. A 503 from the ALB itself confirms the ALB is reachable and the problem is behind it. See the DNS troubleshooting guide if errors appear before the ALB responds.