TargetGroupUnhealthy

Load Balancer Target Group Unhealthy: Fix in 10 Steps

Server & Cloud Intermediate 👁 8 views 📅 Jun 16, 2026

Your AWS or Azure load balancer marks targets unhealthy. We'll fix health check configs, firewall rules, and app responses.

When This Error Hits

You're deploying a new app behind an AWS Application Load Balancer (ALB) or Azure Load Balancer. You register EC2 instances or VMs in the target group. The load balancer dashboard shows them as unhealthy. You refresh, wait 5 minutes, still red. Your users get 503s or timeouts. The trigger is almost always a mismatch between what the health check expects and what your app actually serves.

Root Cause in Plain English

The load balancer sends a GET request to your app on a specific path (like /health) every few seconds. It expects a 200 or 302 response within a timeout. If your app returns anything else — a 404, a 500, or just hangs — the target gets marked unhealthy. 9 times out of 10 it's one of three things: wrong health check path, a firewall blocking the load balancer's IPs, or the app itself not handling that path. Let's fix it.

Step-by-Step Fix

  1. Verify the health check path on the load balancer side.
    In AWS, go to EC2 > Target Groups > select your target group > Health checks tab. For Azure, it's under Load Balancer > Health probes. The path must match something your app serves. Common paths: /, /health, /healthz, /api/health. If you set it to /status but your app only has /health, the check will fail. Change it to match.
  2. Check what your app actually returns on that path.
    SSH into one of the targets. Run:
    curl -v http://localhost:8080/health
    (adjust port to your app's port). You should see a 200 or 302 status code. If you get a 404, your app doesn't serve that path. If you get a 5xx, your app is crashing. If curl hangs, the app isn't listening on that port. Fix the app or change the health check path to one that exists.
  3. Confirm the load balancer can reach the target's port.
    The target group has a port (like 80, 443, or 8080). Make sure your app is listening on that exact port. On the target, run:
    ss -tlnp | grep :8080
    If nothing shows, your app isn't running or is on a different port. Start it or change the target group port.
  4. Open the security group or NSG to the load balancer.
    In AWS, the target's security group must allow inbound traffic from the load balancer's security group. In Azure, the Network Security Group (NSG) must allow inbound from the Azure Load Balancer's virtual IP (168.63.129.16) or the load balancer's backend pool subnet. Don't open to 0.0.0.0/0 — that's lazy. Only allow the load balancer's source. Apply the rule, then check the target group status again.
  5. Check the health check interval and timeout.
    Default interval is 30 seconds, timeout is 5 seconds. If your app takes 8 seconds to respond (maybe because of a cold start or a slow database query), the health check times out. Lower the timeout won't help — fix the app response time. Or increase the interval to 60 seconds and the unhealthy threshold to 3 to give it more leeway. Don't do this as a permanent crutch; fix the app.
  6. Test the health check from a different machine.
    From a box that's not the target itself, run:
    curl -v http://TARGET_IP:8080/health
    Use the private IP of the target. If this works but the load balancer still says unhealthy, the problem is likely the security group or network ACL blocking the load balancer's range. If it hangs, you have a firewall or routing issue.
  7. For HTTP health checks, check the response body.
    Some load balancers (like AWS ALB) only care about the status code. But if you're using a custom health checker that parses the body (like a bash script), make sure the response contains the expected keyword. Most setups don't need this, but I've seen people set a success code of 200 and the app returns a 200 with "{\"status\":\"ok\"}" — that works fine. If you get tricky with success codes above 399, the check will fail.
  8. Check if the app has rate limiting or IP whitelisting.
    Your app might have middleware that blocks requests from unknown IPs. The load balancer's health check comes from a specific range of IPs. If your app blocks them, the health check gets a 403 or a 429. Whitelist the load balancer's source IPs. In AWS, that's the load balancer's subnet IPs. In Azure, it's 168.63.129.16.
  9. Restart the app and the load balancer.
    On the target, restart the application:
    sudo systemctl restart your-app
    Then wait 30 seconds. Sometimes a zombie process is holding the port. Also, in AWS, you can deregister the target and register it again. Go to Target Groups > select the target > Actions > Deregister. Wait 60 seconds, then register it again. This forces a fresh health check.
  10. Check the load balancer logs if it still fails.
    In AWS, enable access logs on the ALB. Check the S3 bucket for the target group log entries. Look for lines with elb_status_code of 503 or 0. The target_status_code will show what the target returned. If it's a dash (-), the request never reached the target — that's a network or security group issue. If it's a 404 or 500, fix the app.

If It Still Fails After All That

A few edge cases I've seen:

  • Multiple load balancers pointing to the same target group. Unlikely but possible — check you're looking at the right one.
  • Target group is in a different VPC or region. Cross-region target groups don't work for ALBs. Make sure they're in the same VPC.
  • The app needs a host header. Some apps (especially virtual-host-based) return 404 if the host header doesn't match. The health check sends the target's IP as the host. If your app requires a specific hostname, configure the health check to send a custom host header. In AWS, you can set this in the health check settings under "Advanced health check details".
  • IPv6 vs IPv4. If your target group uses IPv6 but your app only listens on IPv4, the health check will fail. Make sure your app binds to 0.0.0.0 or both addresses.
  • The load balancer just needs time. After making changes, wait the full health check interval (up to 60 seconds) plus the unhealthy threshold (default 2 checks). That's up to 3 minutes. Don't panic-refresh every 5 seconds.

If you've done all ten steps, checked the logs, and waited, and it's still unhealthy — open a support ticket with AWS or Azure. Include the health check configuration, the curl output from the target, and the security group rules. They'll be able to see what you can't.

Was this solution helpful?