curl-Retry-Strategien für instabile APIs
AI generated
$_
#!/
Bash · curl · APIs · Fehlerbehandlung
curl-Retry-Strategien für instabile APIs
Wann curl --retry reicht und wann ein eigenes Backoff im Skript nötig ist

Instabile APIs antworten mal mit einem kurzen Timeout, mal mit einem 503, mal einfach gar nicht. curl bringt mit --retry, --retry-delay und --retry-connrefused eingebautes Wiederholungsverhalten mit, das viele dieser Fälle abdeckt, aber erst das Verständnis von Idempotenz entscheidet, ob ein Retry überhaupt sicher ist, ohne Daten doppelt zu verarbeiten.

16 Min. Lesezeit curl --retry · Backoff Bash · REST-APIs · Fehlerbehandlung

1. Transiente Fehler von permanenten Fehlern unterscheiden

Nicht jeder Fehlschlag beim Aufruf einer API ist gleich zu behandeln. Ein transienter Fehler, etwa ein kurzzeitiger Timeout, ein 502 vom Load Balancer während eines Deployments beim Anbieter, oder ein 503 wegen kurzzeitiger Überlast, löst sich oft von selbst, wenn der Aufruf nach einer kurzen Pause wiederholt wird. Ein permanenter Fehler, etwa ein 401 wegen eines ungültigen Tokens oder ein 404, weil die Ressource schlicht nicht existiert, ändert sich dagegen durch Wiederholen nicht und verschwendet nur Zeit, wenn ein Skript es trotzdem erneut versucht.

Ein robustes Bash-Skript, das gegen eine instabile API arbeitet, muss diese Unterscheidung explizit treffen, statt jeden Fehlschlag pauschal zu wiederholen oder pauschal aufzugeben. curl bietet dafür bereits eingebaute Mechanismen, die für die häufigsten transienten Fälle ausreichen, aber nicht jede Situation automatisch richtig einordnen können.

2. curl --retry: welche Fehler standardmäßig wiederholt werden

Die Option --retry <anzahl> weist curl an, einen fehlgeschlagenen Aufruf bis zu der angegebenen Anzahl Mal zu wiederholen. Wiederholt werden dabei standardmäßig nur Fehler, die curl selbst als transient einstuft: DNS-Auflösungsfehler, Timeouts, bestimmte Verbindungsprobleme und HTTP-Statuscodes, die auf eine vorübergehende Serverüberlastung hindeuten, etwa 408, 429, 500, 502, 503 und 504.

Ein einfacher HTTP-Fehlerstatus wie 404 oder 401 löst dagegen keinen Retry aus, selbst wenn --retry gesetzt ist, weil curl davon ausgeht, dass ein erneuter Versuch an diesem Ergebnis nichts ändern würde. Diese eingebaute Unterscheidung deckt bereits einen großen Teil der Fälle sinnvoll ab, ohne dass ein Bash-Skript selbst HTTP-Statuscodes auswerten muss.


#!/usr/bin/env bash
set -euo pipefail

# Retries up to 5 times, but only for statuses curl considers transient
curl --retry 5 \
     --retry-delay 2 \
     --fail \
     --silent \
     --show-error \
     "https://api.example.com/v1/orders/42"

3. --retry-delay und das eingebaute exponentielle Backoff

Ohne explizite Angabe wartet curl zwischen Wiederholungsversuchen nicht immer gleich lange, sondern erhöht die Wartezeit von Versuch zu Versuch automatisch, ein einfaches exponentielles Backoff, das standardmäßig bei einer Sekunde beginnt und sich mit jedem weiteren Fehlschlag verdoppelt, begrenzt durch eine interne Obergrenze. Das verhindert, dass ein Skript eine bereits überlastete API mit dicht aufeinanderfolgenden Wiederholungsversuchen zusätzlich unter Druck setzt.

Mit --retry-delay <sekunden> lässt sich dieses Verhalten überschreiben und auf eine feste Wartezeit zwischen allen Versuchen umstellen. Das ist sinnvoll, wenn die Zielanwendung ein bekanntes, festes Zeitfenster für die Erholung braucht, etwa weil ein Health-Check-Intervall auf der Serverseite exakt bekannt ist, aber im Regelfall ist das automatische exponentielle Verhalten von curl die bessere Standardeinstellung, weil es sich selbstständig an die Fehlerhäufigkeit anpasst.


#!/usr/bin/env bash
set -euo pipefail

# Fixed 3s delay between every retry, overriding curl's own exponential backoff
curl --retry 4 \
     --retry-delay 3 \
     --retry-max-time 60 \
     --fail \
     "https://api.example.com/v1/status"

4. --retry-connrefused: warum Connection Refused nicht automatisch wiederholt wird

Ein Connection Refused tritt auf, wenn zwar ein Host erreichbar ist, aber kein Prozess auf dem angefragten Port lauscht, etwa weil der Zieldienst gerade neu startet oder abgestürzt ist. curl behandelt diesen Fehler standardmäßig als endgültig und wiederholt ihn selbst mit gesetztem --retry nicht automatisch, weil ein verweigerter Verbindungsaufbau in vielen klassischen Szenarien tatsächlich dauerhaft ist, etwa bei einem falsch konfigurierten Port.

In modernen Cloud- und Container-Umgebungen ist ein kurzzeitiges Connection Refused während eines Rolling-Deployments oder eines Container-Neustarts aber ein völlig normaler, transienter Zustand. Die Option --retry-connrefused weist curl explizit an, auch diesen Fehler in die Wiederholungslogik einzubeziehen, was für Deployment- und Monitoring-Skripte gegen dynamische Infrastruktur fast immer die richtige Einstellung ist.


#!/usr/bin/env bash
set -euo pipefail

# Treat "connection refused" as retryable too -- common during rolling
# deployments when the target container briefly has no listener
curl --retry 6 \
     --retry-delay 5 \
     --retry-connrefused \
     --fail \
     "https://api.example.com/v1/health"

5. Wann exponentielles Backoff im eigenen Skript sinnvoller ist

curls eingebautes Retry stößt an seine Grenzen, sobald die Entscheidung zum Wiederholen von etwas anderem als dem HTTP-Statuscode abhängt, etwa vom Inhalt der Antwort, von einem Retry-After-Header, den der Server für eine exakte Wartezeit mitschickt, oder von Geschäftslogik, die zwischen zwei Wiederholungsversuchen andere Aktionen ausführen muss, etwa das Erneuern eines abgelaufenen Tokens. In diesen Fällen ist ein eigenes Backoff im Bash-Skript die bessere Wahl, weil es volle Kontrolle über die Entscheidungslogik zwischen den Versuchen erlaubt.

Ein selbstgebautes Backoff liest den HTTP-Statuscode und relevante Header separat aus, wartet bei einem 429 gezielt die im Retry-After-Header angegebene Zeit statt einer pauschalen Wartezeit, und kann bei einem 401 zuerst versuchen, das Token zu erneuern, bevor der eigentliche Aufruf wiederholt wird, was curls eingebautes Retry so nicht abbilden kann.


#!/usr/bin/env bash
set -euo pipefail

url="https://api.example.com/v1/orders"
max_attempts=5
attempt=1

while (( attempt <= max_attempts )); do
  response="$(curl --silent --write-out '\n%{http_code}' "$url")"
  status="${response##*$'\n'}"
  body="${response%$'\n'*}"

  if [[ "$status" == "200" ]]; then
    echo "$body"
    exit 0
  elif [[ "$status" == "429" ]]; then
    retry_after="$(curl --silent -I "$url" | grep -i '^retry-after:' | tr -d '\r' | cut -d' ' -f2)"
    echo "Rate limited, honoring Retry-After: ${retry_after:-5}s" >&2
    sleep "${retry_after:-5}"
  else
    delay=$(( 2 ** attempt ))
    echo "Attempt $attempt failed with $status, backing off ${delay}s" >&2
    sleep "$delay"
  fi
  (( attempt++ ))
done

echo "All $max_attempts attempts failed" >&2
exit 1

6. Idempotenz als Voraussetzung für sicheres Retry

Ein Wiederholungsversuch ist nur dann gefahrlos, wenn der ursprüngliche Aufruf beim ersten Versuch tatsächlich fehlgeschlagen ist, bevor der Server ihn verarbeitet hat. Bei einem Timeout ist das unklar: Die Anfrage könnte auf dem Server bereits vollständig ausgeführt worden sein, nur die Antwort ist nie beim Client angekommen. Ein GET erneut zu senden ist unproblematisch, weil es per Definition keine Seiteneffekte hat, aber ein POST zum Anlegen einer neuen Bestellung nach einem Timeout einfach zu wiederholen kann dazu führen, dass dieselbe Bestellung zweimal angelegt wird.

Für nicht von Natur aus idempotente Operationen wie POST ist deshalb ein Idempotency-Key das entscheidende Muster: Das Skript erzeugt vor dem ersten Versuch einen eindeutigen Schlüssel und sendet ihn bei jedem Wiederholungsversuch identisch mit. Ein API-Server, der dieses Muster unterstützt, erkennt anhand des Schlüssels, dass es sich um denselben logischen Vorgang handelt, führt ihn nur einmal tatsächlich aus und liefert bei allen weiteren Versuchen einfach das ursprüngliche Ergebnis zurück.


#!/usr/bin/env bash
set -euo pipefail

# Generate one idempotency key BEFORE the first attempt and reuse it
# for every retry -- the server can then safely dedupe on this key
idempotency_key="$(uuidgen)"

curl --retry 4 \
     --retry-delay 2 \
     --retry-connrefused \
     --fail \
     -H "Idempotency-Key: $idempotency_key" \
     -H "Content-Type: application/json" \
     -d '{"product_id": "sku-123", "quantity": 2}' \
     "https://api.example.com/v1/orders"

7. Jitter: warum reines exponentielles Backoff bei vielen Clients ein Problem ist

Ein rein exponentielles Backoff ohne Zufallskomponente funktioniert für ein einzelnes Skript zuverlässig, wird aber problematisch, sobald viele Instanzen desselben Skripts, etwa parallele Cron-Jobs oder mehrere Worker-Container, nach demselben deterministischen Schema warten. Fällt eine API kurz aus, wachen alle wartenden Clients exakt zur selben Sekunde wieder auf und senden ihre Wiederholungsversuche gebündelt, was die gerade erst wieder erreichbare API sofort erneut überlastet, ein Effekt, der als Thundering Herd bekannt ist.

Ein zufälliger Jitter, also eine kleine, zufällige Abweichung von der berechneten Wartezeit, verteilt die Wiederholungsversuche vieler Clients zeitlich, statt sie zu bündeln. In eigenen Backoff-Implementierungen lässt sich das leicht ergänzen, indem zur berechneten Wartezeit ein kleiner zufälliger Betrag addiert wird, bevor sleep aufgerufen wird.

8. Timeouts mit Retry kombinieren, statt sie getrennt zu betrachten

--retry allein schützt nicht davor, dass ein einzelner Versuch selbst unbegrenzt lange hängen bleibt, etwa weil eine Verbindung zwar zustande kommt, aber niemals eine Antwort folgt. Deshalb gehören --connect-timeout für die maximale Zeit bis zum Verbindungsaufbau und --max-time für die maximale Gesamtzeit eines einzelnen Versuchs immer zusammen mit --retry in ein produktionsreifes Skript, sonst kann ein einziger hängender Versuch das gesamte Retry-Budget aufbrauchen, ohne dass je ein zweiter Versuch stattfindet.

Zusätzlich begrenzt --retry-max-time die Gesamtzeit über alle Wiederholungsversuche hinweg, unabhängig von der Anzahl. Das ist besonders in CI-Pipelines wichtig, wo ein einzelner API-Aufruf niemals das gesamte Zeitbudget einer Pipeline-Stufe verbrauchen sollte, egal wie viele Wiederholungsversuche konfiguriert sind.

9. curl-Retry, eigenes Backoff oder Circuit Breaker: die Werkzeugwahl

Für die meisten einfachen API-Aufrufe gegen bekannte, gelegentlich instabile Endpunkte reicht curls eingebautes Retry-Verhalten völlig aus. Sobald Header-basierte Entscheidungen, Idempotenz-Handling oder eine dauerhafte Erkennung eines komplett ausgefallenen Backends nötig sind, lohnt sich der Aufwand für eine eigene Backoff-Logik oder sogar ein Circuit-Breaker-Muster, das nach wiederholten Fehlschlägen ganz auf weitere Versuche verzichtet, bis eine definierte Erholzeit verstrichen ist.

Ansatz Steuerbarkeit Idempotenz-Handling Typischer Einsatz
curl --retry Gering, feste HTTP-Statuscodes Nein, ohne Zusatzlogik Einfache Aufrufe gegen bekannte, gelegentlich instabile APIs
--retry-connrefused Gering, ein zusätzlicher Fehlerfall Nein Deployment- und Health-Check-Skripte gegen dynamische Infrastruktur
Eigenes Backoff im Skript Hoch, volle Kontrolle über Header und Body Ja, mit Idempotency-Key umsetzbar Rate-Limit-Handling, Token-Erneuerung, POST-Aufrufe
Circuit Breaker Hoch, mit Zustand über mehrere Aufrufe hinweg Ja, in Kombination mit Idempotency-Key Dauerhaft instabile Backends, hohe Aufrufraten

Mironsoft

Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur

Shell-Skripte, die in der Produktion zuverlässig laufen?

Wir analysieren bestehende Bash-Skripte, erkennen fragile Muster und ersetzen sie durch robuste Bash-Patterns: mit vollständiger Fehlerbehandlung, Logging und sicherer Parallelisierung für euren Deployment-Stack.

Code-Review

ShellCheck-Analyse und manuelle Prüfung auf kritische Bash-Pattern-Verstöße.

Refactoring

Fehlerbehandlung, Logging und sichere Dateioperationen nachrüsten.

CI-Integration

ShellCheck und BATS in Pipelines integrieren und Regressionstests aufbauen.

10. Zusammenfassung

curl-Retry in Bash: Das Wichtigste auf einen Blick

Grundprinzip

curl --retry wiederholt automatisch bei transienten Fehlern wie Timeouts und 502/503, nicht bei 404 oder 401.

Backoff

curl backt exponentiell ab, --retry-delay erzwingt eine feste Wartezeit, --retry-connrefused deckt Container-Neustarts ab.

Idempotenz

Ein Idempotency-Key macht auch POST-Aufrufe sicher wiederholbar, ohne doppelte Datensätze zu erzeugen.

Eigenes Backoff

Nötig bei Retry-After-Headern, Token-Erneuerung oder Jitter gegen Thundering Herd bei vielen parallelen Clients.

11. FAQ: curl-Retry in Bash: Das Wichtigste auf einen Blick

1Was macht curl --retry genau?
Es wiederholt einen fehlgeschlagenen Aufruf bis zur angegebenen Anzahl, aber nur bei Fehlern, die curl als transient einstuft, etwa Timeouts oder HTTP-Statuscodes wie 502 und 503.
2Wiederholt curl auch bei einem 404?
Nein. HTTP-Statuscodes wie 404 oder 401 gelten als endgültig und lösen selbst bei gesetztem --retry keinen Wiederholungsversuch aus.
3Was macht --retry-delay?
Es überschreibt curls automatisches exponentielles Backoff mit einer festen Wartezeit zwischen allen Wiederholungsversuchen.
4Warum wird Connection Refused nicht automatisch wiederholt?
curl stuft es standardmäßig als endgültigen Fehler ein. Mit --retry-connrefused lässt sich das explizit ändern, was bei Rolling-Deployments sinnvoll ist.
5Wann brauche ich ein eigenes Backoff statt curl --retry?
Wenn die Entscheidung von Response-Headern wie Retry-After abhängt, wenn zwischen Versuchen ein Token erneuert werden muss, oder wenn Jitter gegen Thundering Herd nötig ist.
6Was ist ein Idempotency-Key und wofür brauche ich ihn?
Ein eindeutiger, vor dem ersten Versuch erzeugter Schlüssel, der bei jedem Wiederholungsversuch identisch mitgesendet wird. Er erlaubt dem Server, doppelte Ausführung eines nicht von Natur aus idempotenten Aufrufs wie POST zu erkennen.
7Ist ein GET-Aufruf immer sicher zu wiederholen?
In der Regel ja, weil GET per Definition keine Seiteneffekte haben soll. Bei APIs, die diese Konvention nicht einhalten, gilt das aber nicht automatisch.
8Was ist Thundering Herd bei Retries?
Wenn viele Clients nach demselben deterministischen Backoff-Schema warten, wachen sie gleichzeitig auf und überlasten eine gerade erst wieder erreichbare API erneut. Jitter verhindert das.
9Wie kombiniere ich Timeouts mit Retry sinnvoll?
--connect-timeout und --max-time begrenzen einen einzelnen Versuch, --retry-max-time begrenzt alle Versuche zusammen, damit kein hängender Versuch das gesamte Retry-Budget verbraucht.
10Wann lohnt sich ein Circuit Breaker statt einfachem Retry?
Wenn ein Backend dauerhaft ausfällt und wiederholte Versuche nur unnötig Last erzeugen. Ein Circuit Breaker stoppt weitere Versuche für eine definierte Zeit, statt jeden Aufruf einzeln zu wiederholen.