ALB gibt 502 Bad Gateway zurück – Ursachen und Diagnose wenn Targets als 'Healthy' angezeigt werden

Der Application Load Balancer meldet 502, die Target Group zeigt alle Instanzen als 'Healthy' – und genau diese Kombination ist das Problem. Wer zuerst auf die Health Checks schaut und dort grüne Häkchen sieht, sucht danach meistens an der falschen Stelle weiter. Ein 502 vom ALB bedeutet nicht, dass die Instanz nicht erreichbar ist, sondern dass der ALB eine Antwort erhalten hat, die er nicht verarbeiten konnte.

TL;DR – ALB 502 Bad Gateway Schnellübersicht

UrsacheSymptomErste Prüfung
Ungültige HTTP-Antwort vom Backend502 bei allen oder bestimmten RequestsAccess Logs: target_status_code
Keep-Alive Timeout MismatchIntermittierende 502, kein App-Fehler im LogKeep-Alive-Konfiguration des App-Servers prüfen
Verbindung vom Backend vorzeitig getrennt502 nach variabler AntwortzeitALB Access Log: target_status_code leer oder -
Protokoll-Mismatch (HTTP vs. HTTPS)502 sofort, TLS-Fehler im Backend-LogTarget Group Protokoll vs. App-Server-Port
Response Header zu groß502 bei spezifischen EndpunktenHeader-Größe der Antwort messen
Chunked Encoding Fehler502 bei großen ResponsesTransfer-Encoding Header prüfen

Wie der ALB Verbindungen verwaltet – Grundlage für die 502-Diagnose

Bevor die Diagnose beginnt, muss das Verbindungsmodell des ALB klar sein. Der ALB terminiert jede eingehende Client-Verbindung und baut eine separate Verbindung zum Backend-Target auf. Das sind zwei vollständig unabhängige TCP-Verbindungen. Ein 502 entsteht ausschließlich auf der Backend-Seite – also zwischen ALB und Target.

Health Checks und Request-Handling laufen über unterschiedliche Verbindungen. Eine Instanz kann Health-Check-Requests korrekt beantworten und trotzdem bei echten Requests einen 502 produzieren – etwa weil der Health-Check-Pfad eine triviale 200-Antwort liefert, während der eigentliche Endpunkt fehlerhafte HTTP-Header zurückgibt.

graph LR Client(["Client"]) ALB["Application Load Balancer"] TG["Target Group"] EC2["EC2 Instanz
(App-Server)"] HC["Health Check
Verbindung"] REQ["Request
Verbindung"] Client -->|"TCP Verbindung 1
(terminiert am ALB)"| ALB ALB -->|"TCP Verbindung 2
(separate Verbindung)"| REQ REQ --> EC2 ALB -.->|"Unabhängig"| HC HC -.-> EC2 TG --> EC2 style HC stroke-dasharray: 5 5 style REQ fill:#f9f,stroke:#333
  1. Client → ALB: TCP-Verbindung wird vom ALB terminiert. Der Client kommuniziert ausschließlich mit dem ALB.
  2. ALB → Target: Separate TCP-Verbindung. Der ALB leitet den Request weiter und erwartet eine valide HTTP-Antwort.
  3. 502-Entstehung: Wenn das Target keine gültige HTTP-Antwort liefert, die Verbindung abbricht oder der ALB die Antwort nicht parsen kann, gibt der ALB dem Client einen 502 zurück.
  4. Health Check: Läuft parallel, auf einem konfigurierten Pfad. Ein erfolgreicher Health Check schließt einen 502 bei echten Requests nicht aus.

Schritt 1: ALB Access Logs aktivieren und auswerten

Ohne Access Logs ist die Diagnose eines ALB 502 reines Raten. Der entscheidende Feldwert ist target_status_code. Wenn dieses Feld einen Bindestrich (-) enthält, hat das Backend gar keine Antwort geliefert – die Verbindung wurde abgebrochen, bevor eine HTTP-Antwort übertragen wurde. Das ist ein anderes Problem als ein Backend, das aktiv einen Fehlercode zurückgibt.

Access Logs für einen bestehenden ALB aktivieren:

aws elbv2 modify-load-balancer-attributes \
  --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/mein-alb/1234567890abcdef \
  --attributes Key=access_logs.s3.enabled,Value=true \
               Key=access_logs.s3.bucket,Value=mein-alb-logs-bucket \
               Key=access_logs.s3.prefix,Value=alb-logs

Das S3-Bucket benötigt eine Bucket Policy, die dem ALB-Service-Account das Schreiben erlaubt. Die erforderliche Policy ist in der offiziellen AWS-Dokumentation für Elastic Load Balancing Access Logs beschrieben – der genaue Principal variiert je nach Region.

Nach dem Aktivieren der Logs relevante Einträge filtern. Mit AWS CLI und S3 Select oder direkt über Athena lassen sich die Logs effizient abfragen. Für einen schnellen ersten Blick:

aws s3 cp s3://mein-alb-logs-bucket/alb-logs/ ./alb-logs/ --recursive \
  --exclude '*' --include '*.log.gz'

# Komprimierte Logs nach 502-Einträgen durchsuchen
zgrep '" 502 ' ./alb-logs/*.log.gz | head -50

Die kritischen Felder im Access Log für die 502-Diagnose sind elb_status_code (immer 502), target_status_code (was das Backend geantwortet hat oder -), und error_reason (seit neueren ALB-Versionen verfügbar, enthält den Grund für den 502).

Schritt 2: target_status_code auswerten – die entscheidende Weiche

Das Feld target_status_code im Access Log teilt die möglichen Ursachen in zwei klar getrennte Kategorien:

graph TD START(["ALB Access Log analysieren"]) CHECK{"target_status_code Wert?"} DASH["Bindestrich: -"] CODE["HTTP Statuscode vorhanden"] DASH_CAUSE["Verbindungsabbruch vom Backend"] CODE_CAUSE["Ungültige HTTP-Antwort empfangen"] KA["Keep-Alive Timeout prüfen"] CRASH["App-Absturz / Verbindungslimit"] HEADER["Response Header prüfen"] PROTO["Protokoll-Mismatch prüfen"] CHUNK["Chunked Encoding prüfen"] START --> CHECK CHECK --> DASH CHECK --> CODE DASH --> DASH_CAUSE CODE --> CODE_CAUSE DASH_CAUSE --> KA DASH_CAUSE --> CRASH CODE_CAUSE --> HEADER CODE_CAUSE --> PROTO CODE_CAUSE --> CHUNK style DASH fill:#ffcccc style CODE fill:#ffffcc style KA fill:#ccffcc style HEADER fill:#ccffcc
  1. target_status_code ist '-': Das Backend hat die Verbindung abgebrochen oder gar nicht geantwortet. Ursachen: Keep-Alive Timeout, Prozessabsturz, Verbindungslimit erreicht.
  2. target_status_code enthält einen Wert (z.B. 200, 500): Das Backend hat geantwortet, aber die Antwort war HTTP-technisch ungültig – fehlerhafte Header, ungültiges Chunked Encoding, oder zu große Header.
  3. error_reason Feld: Wenn vorhanden, liefert es den konkreten Fehlergrund direkt aus dem ALB, z.B. TargetConnectionErrorCode oder ähnliche Bezeichner.

Schritt 3: Keep-Alive Timeout Mismatch – die häufigste versteckte Ursache

Intermittierende 502-Fehler ohne App-Fehler im Backend-Log und mit leerem target_status_code deuten fast immer auf einen Keep-Alive Timeout Mismatch hin. Der ALB hält Verbindungen zu Backends offen und wiederverwendet sie. Wenn der App-Server seine Keep-Alive-Verbindung schließt, bevor der ALB das mitbekommt, sendet der ALB den nächsten Request auf einer bereits geschlossenen Verbindung – und erhält keine Antwort.

Das ist wie ein Taxifahrer, der aussteigt und die Tür zumacht, während der Fahrgast noch einsteigen will. Der Fahrgast sieht ein leeres Taxi – nicht einen Fahrer, der 'Nein' sagt.

Der ALB hat einen eigenen Idle Timeout (Standard: 60 Sekunden). Der App-Server muss seinen Keep-Alive Timeout auf einen Wert setzen, der höher ist als der ALB Idle Timeout. Sonst schließt der App-Server die Verbindung, bevor der ALB sie als inaktiv erkennt.

ALB Idle Timeout prüfen:

aws elbv2 describe-load-balancer-attributes \
  --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/mein-alb/1234567890abcdef \
  --query 'Attributes[?Key==`idle_timeout.timeout_seconds`]'

Idle Timeout anpassen (Beispiel: auf 120 Sekunden erhöhen):

aws elbv2 modify-load-balancer-attributes \
  --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/mein-alb/1234567890abcdef \
  --attributes Key=idle_timeout.timeout_seconds,Value=120

Für gängige App-Server gilt: Der Keep-Alive Timeout des App-Servers muss größer sein als der ALB Idle Timeout. Bei nginx ist das keepalive_timeout, bei Apache KeepAliveTimeout. Wer den ALB Idle Timeout auf 60 Sekunden lässt und nginx auf 65 Sekunden konfiguriert, ist auf der sicheren Seite.

Schritt 4: Protokoll-Mismatch zwischen Target Group und App-Server

Wenn die Target Group auf HTTPS konfiguriert ist, der App-Server aber nur HTTP auf dem Port hört – oder umgekehrt – bricht die Verbindung sofort ab. Der ALB versucht einen TLS-Handshake, der App-Server antwortet mit rohem HTTP, und der ALB kann die Antwort nicht interpretieren.

Target Group Protokoll prüfen:

aws elbv2 describe-target-groups \
  --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/mein-alb/1234567890abcdef \
  --query 'TargetGroups[*].{Name:TargetGroupName,Protocol:Protocol,Port:Port}'

Wenn das Protokoll HTTPS ist, muss der App-Server TLS auf dem konfigurierten Port terminieren. Wenn HTTP, darf kein TLS auf dem Port aktiv sein. Das klingt offensichtlich, aber in der Praxis passiert dieser Fehler häufig nach Migrationen, wenn die Target Group neu erstellt wird und das Protokoll nicht explizit gesetzt wird.

Schritt 5: Ungültige HTTP-Antwort-Header vom Backend

Der ALB ist strikt bei der HTTP-Konformität. Antworten mit ungültigen Header-Werten, Header-Namen mit unerlaubten Zeichen, oder Responses, die gegen den HTTP/1.1-Standard verstoßen, werden mit einem 502 abgelehnt. Das Backend-Log zeigt in diesem Fall eine erfolgreiche Antwort – aus Sicht des App-Servers hat alles funktioniert.

Dieser Fall ist besonders tückisch: Das Backend-Log zeigt 200, der Client sieht 502, und der ALB Access Log zeigt target_status_code als 200. Das bedeutet: Der ALB hat die Antwort empfangen, aber beim Parsen der Headers ist etwas schiefgelaufen.

Direkte Verbindung zum Backend testen, um die rohen HTTP-Headers zu sehen:

# Direkt gegen die EC2-Instanz testen (Security Group muss temporären Zugriff erlauben)
curl -v http://10.0.1.50:8080/api/endpoint 2>&1 | grep -E '^[<>]'

Auf folgende Probleme in den Response-Headers achten:

  • Header-Namen mit Leerzeichen oder Sonderzeichen
  • Mehrfach gesetzte Content-Length Header mit unterschiedlichen Werten
  • Ungültige Transfer-Encoding Werte
  • Header-Werte mit Steuerzeichen (CR, LF innerhalb eines Header-Werts)

Schritt 6: CloudWatch Metriken für Muster-Erkennung

Bevor einzelne Requests manuell analysiert werden, lohnt es sich, die zeitliche Verteilung der 502-Fehler zu verstehen. Treten sie gleichmäßig auf, oder gibt es Spitzen? Korrelieren sie mit bestimmten Zeiten oder Deployments?

aws cloudwatch get-metric-statistics \
  --namespace AWS/ApplicationELB \
  --metric-name HTTPCode_ELB_5XX_Count \
  --dimensions Name=LoadBalancer,Value=app/mein-alb/1234567890abcdef \
  --start-time 2024-01-15T00:00:00Z \
  --end-time 2024-01-15T23:59:59Z \
  --period 300 \
  --statistics Sum

Zusätzlich die Metrik HTTPCode_Target_5XX_Count prüfen. Wenn HTTPCode_ELB_5XX_Count hoch ist, aber HTTPCode_Target_5XX_Count niedrig oder null, kommen die 502-Fehler vom ALB selbst – das Backend antwortet gar nicht oder mit ungültigen Responses.

aws cloudwatch get-metric-statistics \
  --namespace AWS/ApplicationELB \
  --metric-name HTTPCode_Target_5XX_Count \
  --dimensions Name=LoadBalancer,Value=app/mein-alb/1234567890abcdef \
  --start-time 2024-01-15T00:00:00Z \
  --end-time 2024-01-15T23:59:59Z \
  --period 300 \
  --statistics Sum

Erfahrung aus der Praxis: Der 502 der erst nach Deployment auftrat

Symptom: Nach einem Deployment begannen sporadische 502-Fehler, etwa 2-3% aller Requests. Die Instanzen waren 'Healthy', das Application-Log zeigte keine Fehler, und die 502-Fehler traten ohne erkennbares Muster auf.

Erste Annahme: Das neue Deployment hatte einen Bug eingeführt, der bestimmte Requests zum Absturz brachte. Also: Rollback. Die 502-Fehler blieben.

Tatsächliche Ursache: Das Deployment hatte die nginx-Konfiguration geändert – konkret den keepalive_timeout von 75 auf 30 Sekunden reduziert, um 'Ressourcen zu sparen'. Der ALB Idle Timeout war auf 60 Sekunden konfiguriert. Nginx schloss Verbindungen nach 30 Sekunden, der ALB hielt sie für weitere 30 Sekunden offen und sendete neue Requests auf bereits geschlossenen Verbindungen.

Der Beweis im Access Log: target_status_code war in allen 502-Einträgen ein Bindestrich. Kein Backend-Fehler – die Verbindung war einfach schon zu.

Fix: keepalive_timeout in nginx auf 75 Sekunden zurückgesetzt (höher als der ALB Idle Timeout von 60 Sekunden). 502-Fehler verschwanden sofort.

Das Lehrreiche daran: Der Rollback hat nicht geholfen, weil das Problem nicht im Anwendungscode lag, sondern in der Webserver-Konfiguration, die separat deployed wurde. Wer nur den App-Code zurückrollt, lässt die nginx-Config unberührt.

IAM-Berechtigungen für die Diagnose

Für die oben beschriebenen CLI-Befehle sind folgende Mindestberechtigungen erforderlich:

🔽 IAM Policy für ALB 502-Diagnose anzeigen
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ALBReadAccess",
      "Effect": "Allow",
      "Action": [
        "elasticloadbalancing:DescribeLoadBalancers",
        "elasticloadbalancing:DescribeLoadBalancerAttributes",
        "elasticloadbalancing:DescribeTargetGroups",
        "elasticloadbalancing:DescribeTargetHealth"
      ],
      "Resource": "*"
    },
    {
      "Sid": "ALBModifyAttributes",
      "Effect": "Allow",
      "Action": [
        "elasticloadbalancing:ModifyLoadBalancerAttributes"
      ],
      "Resource": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/mein-alb/1234567890abcdef"
    },
    {
      "Sid": "CloudWatchReadAccess",
      "Effect": "Allow",
      "Action": [
        "cloudwatch:GetMetricStatistics"
      ],
      "Resource": "*"
    },
    {
      "Sid": "S3LogAccess",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::mein-alb-logs-bucket",
        "arn:aws:s3:::mein-alb-logs-bucket/*"
      ]
    }
  ]
}

Hinweis: DescribeLoadBalancers und cloudwatch:GetMetricStatistics erfordern "Resource": "*", da diese Aktionen keine ressourcenspezifische Einschränkung unterstützen. Immer die aktuelle Service Authorization Reference prüfen, bevor ARN-Einschränkungen angewendet werden.

ALB 502 Bad Gateway – Zusammenfassung und nächste Schritte

Ein 502 vom ALB bei gleichzeitig 'Healthy' angezeigten Targets ist kein Widerspruch – Health Checks und Request-Handling sind vollständig getrennte Verbindungen. Die Diagnose beginnt immer mit den ALB Access Logs und dem Feld target_status_code: Bindestrich bedeutet Verbindungsabbruch, ein vorhandener Statuscode bedeutet ungültige HTTP-Antwort.

Die häufigste Ursache in produktiven Umgebungen ist der Keep-Alive Timeout Mismatch – der App-Server-Timeout muss immer größer sein als der ALB Idle Timeout. Das ist keine offensichtliche Abhängigkeit, und sie bricht regelmäßig nach Konfigurationsänderungen, die scheinbar nichts mit dem Load Balancer zu tun haben.

Weiterführende Ressourcen:

Glossar

BegriffBedeutung
502 Bad GatewayHTTP-Statuscode: Der Proxy (ALB) hat vom Backend eine ungültige Antwort erhalten oder gar keine Antwort bekommen.
Keep-Alive TimeoutZeitspanne, nach der ein HTTP-Server eine persistente Verbindung ohne Aktivität schließt. Muss beim App-Server größer sein als der ALB Idle Timeout.
target_status_codeFeld im ALB Access Log: HTTP-Statuscode der Backend-Antwort. Ein Bindestrich (-) bedeutet, dass keine Antwort empfangen wurde.
ALB Idle TimeoutKonfigurierbare Zeitspanne (Standard: 60 Sekunden), nach der der ALB eine inaktive Verbindung schließt.
Protokoll-MismatchKonfigurationsfehler, bei dem Target Group und App-Server unterschiedliche Protokolle (HTTP vs. HTTPS) verwenden.

Kommentare

Beliebte Posts aus diesem Blog

EC2 ohne Internetzugang im eigenen VPC – Internet Gateway und Route Table korrekt einrichten

EC2 SSH Verbindungs-Timeout: Security Group Inbound-Regeln richtig konfigurieren