Als 502 aus dem Nichts kam — Nginx-Origin-Konfiguration reparieren

Alt-Text Nach einem 502 in Produktion.

Incident

Last Tuesday, 2:04 PM — das Monitoring schlug Alarm. Nicht einmal rot, sondern direkt stumm. Die Seite war da, die Startseite lud, aber zwei API-Endpunkte lieferten nur noch 502 Bad Gateway. Alle Logs zeigten denselben kurzen Eintrag:

[error] 12#12: *57923 connect() failed (111: Connection refused) while connecting to upstream

Kein Crash, kein Rollback, kein Deploy in der Stunde davor. Der Nginx lief, der Upstream auch — aber der einen Weg dazwischen war plötzlich tot.

Root Cause

Ich habe zwei Fehler gemacht — und beide waren unscheinbar.

Erstens hatte ich am Origin keepalive_timeout auf 5s heruntergesetzt, weil der Server angeblich Ressourcen schonen sollte. Das ist an sich kein Problem, solange der Client das gleiche oder ein kleineres Limit nutzt.

Zweitens fehlte auf dem Nginx der explizite proxy_next_upstream-Block. Nginx ist hier pingelig: Wenn der erste Upstream-Verbindungsversuch wegen eines Timeouts fehlschlägt, wird bei einem konfigurierten proxy_next_upstream der nächste probiert — oder zumindest sauber abgebrochen. Ohne diesen Block bleibt der Request im Gateway hängen, und der Client sieht am Ende den 502.

Der Worst Case trat ein, als der Origin unter Last kurzzeitig keine neuen Verbindungen mehr akzeptierte. Nginx wartete, der Client wartete, und beide Timeouts liefen unkoordiniert auseinander.

Lösung

Der Fix besteht aus drei Teilen: Keepalive wieder anheben, explizites Next-Upstream-Verhalten setzen und die Client-Timeout-Kette konsistent machen.

1. Keepalive wieder anheben

# /etc/nginx/conf.d/upstream.conf
# Mindestens so hoch wie der Client-Timeout,
# sonst schließt der Origin Verbindungen, bevor Nginx sie nutzt.
keepalive_timeout 65;

2. Upstream-Verbindungspool gescheit konfigurieren

upstream api_backend {
    server 10.0.1.20:8080;

    # Max offene Keepalive-Verbindungen zum Origin.
    # 64 reicht für kleine bis mittlere Dienste.
    keepalive 64;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    location / {
        proxy_pass http://api_backend;

        # Beim ersten Versuch Timeout explizit abschalten,
        # damit Nginx nicht hängen bleibt.
        proxy_connect_timeout 3s;
        proxy_read_timeout 30s;
        proxy_send_timeout 30s;

        # Bei Timeout oder 502 den nächsten Upstream probieren.
        # Ohne diesen Block bleibt der Request hängen.
        proxy_next_upstream error timeout http_502 http_503;
    }
}

3. Client-Timeout an den Browser anpassen

# Oberste Ebene oder pro Server-Block.
# Passt die maximale Leerlaufzeit einer Keepalive-Verbindung an.
# Sollte nicht kleiner sein als proxy_read_timeout.
keepalive_timeout 65;

client_body_timeout 12s;
client_header_timeout 12s;
send_timeout 10s;

Verifikation

Bevor ich den 502 wiederholt provoziere, prüfe ich erst die Timeout-Kette von beiden Seiten:

# 1. Zeigt, ob Nginx die neuen Werte geladen hat.
nginx -T | grep -E "keepalive_timeout|proxy_read_timeout|proxy_next_upstream"

# 2. Simuliert einen langsamen Upstream und prüft das Verhalten.
# curl zeigt am Ende den HTTP-Status und nicht mehr den blinden 502.
curl -o /dev/null -s -w "%{http_code}\n" --max-time 10 https://api.example.com/health

Die erwartete Ausgabe ist 200. Wenn 502 zurückkommt, fehlt entweder der proxy_next_upstream-Block oder der proxy_connect_timeout ist zu groß gesetzt.

Fazit

Ein 502 ist fast nie ein einzelnes Zeitproblem — er ist meist eine Kette aus zu kurzen oder fehlenden Timeouts auf Client, Gateway und Origin. Der teuerste Fehler war nicht die falsche Zahl, sondern das fehlende proxy_next_upstream: Nginx hat dadurch keine Strategie gehabt, wenn der erste Versuch scheitert. Danach zu suchen, spart Zeit.

Die Lehre ist einfach: Immer alle drei Seiten — Client, Nginx, Origin — aufeinander abstimmen und den Fallback explizit konfigurieren, statt auf Standardverhalten zu hoffen.

Siehe auch:

  • [Wenn die Datenbank blockiert — PostgreSQL-Lockstürme](/blog/wenn-die-datenbank-blockiert-postgresql-lock-stuerme/)
  • [Meine Pipeline schlug fehl, weil Connections im Nichts verschwanden](/blog/meine-pipeline-schlug-fehl-weil-connections-im-nichts-verschwanden/)
  • [Nginx oder Caddy — eine Architekturentscheidung](/blog/nginx-oder-caddy-eine-architekturentscheidung/)