cd ../back to blog
$Guide//June 4, 2026//7 min read

Rate-Limits, Retries und Backoff: produktionsreife Fehlerbehandlung für KI-APIs

Behandle 429er und 5xx richtig: exponentielles Backoff mit Jitter, Retry-After beachten, Idempotenz, Timeouts und Circuit Breaker für KI-API-Calls.

Die Demo lief beim ersten Call. Produktion sind die anderen 999.999 Calls — die, die während eines Traffic-Spikes in ein Rate-Limit laufen, beim Deploy ein vorgelagertes 503 abbekommen oder an einem Socket hängen, der nie antwortet. Der Unterschied zwischen einer Spielzeug-Integration und einer verlässlichen liegt fast vollständig darin, wie du den unglücklichen Pfad behandelst: was du wiederholst, wie lange du wartest und wann du aufgibst. Mach es falsch, und ein kurzer Anbieter-Schluckauf wird zum selbst verschuldeten Ausfall, weil jeder deiner Worker im perfekten Gleichtakt wiederholt und die Arbeit zu Ende bringt, die der Rate-Limiter begonnen hat.

Das hier ist ein praktischer, produktionsreifer Durchgang: klassifiziere Fehler, damit du nur das Wiederholbare wiederholst, backe exponentiell ab mit Jitter, beachte Retry-After, setze vernünftige Timeouts, mach Wiederholungen mit Idempotenz sicher und hör auf, eine tote Dependency mit einem Circuit Breaker zu malträtieren. Der Code ist OpenAI-SDK-Python gegen https://api.brievio.com/v1, aber die Regeln sind auf jedem HTTP-Client und in jeder Sprache dieselben.

Wiederhole das Wiederholbare — und sonst nichts

Der mit Abstand häufigste Bug bei der Fehlerbehandlung von KI-APIs ist, Dinge zu wiederholen, die nie gelingen werden. Ein 401 (falscher Key), ein 400 (fehlerhafter Request), ein 422 (dein Prompt ist zu lang) — die sind deterministisch. Der zweite Versuch scheitert exakt wie der erste, nur dass es jetzt fünf Versuche und einige Sekunden später sind. Schlimmer noch: Du hast einen echten Bug hinter einer Retry-Schleife versteckt. Das einzige 4xx, das eine Wiederholung wert ist, ist 429 (Rate-Limit), denn das ist vorübergehend.

  • Wiederholen: 429, sowie 500 / 502 / 503 / 504 — das sind vorübergehende Zustände auf Server- oder Kapazitätsseite.
  • Wiederholen: Verbindungsfehler und Read-Timeouts (der Request hat das Modell vielleicht nie erreicht, oder das Modell hat in einen geschlossenen Socket geantwortet).
  • Niemals wiederholen: jedes andere 4xx 400, 401, 403, 404, 422. Mach sie sichtbar, alarmiere darauf, repariere den Aufrufer.

Ein nützlicher Instinkt: Eine Wiederholung ist eine Wette darauf, dass derselbe Request eine andere Antwort bekommt. Das stimmt nur, wenn der Fehler mit Timing oder Kapazität zu tun hatte, nicht mit dem Request selbst.

Exponentielles Backoff mit Jitter

Sobald du weißt, dass ein Fehler wiederholbar ist, lautet die Frage: wie lange warten? Sofort zu wiederholen ist sinnlos — die Bedingung, die das 429 ausgelöst hat, ist eine Millisekunde später immer noch da. Die Standardantwort ist exponentielles Backoff: warte grob 0,5s, dann 1s, 2s, 4s, mit jedem Versuch eine Verdopplung, gedeckelt durch eine Obergrenze, damit eine lange Erholung dich nicht ewig blockiert.

Reines exponentielles Backoff hat im großen Maßstab aber einen bösartigen Fehlermodus. Wenn 500 Worker im selben Augenblick rate-limitiert werden — genau das passiert während eines Spikes — backen sie alle um denselben Betrag ab und wiederholen im selben Augenblick, womit sie den Spike auf einer 2-Sekunden-Uhr neu erzeugen. Das ist die Thundering Herd. Die Lösung ist Jitter: randomisiere jede Verzögerung, sodass sich die Wiederholungen verteilen. Full Jitter (schlafe einen Zufallsbetrag zwischen null und der Backoff-Obergrenze) entkoppelt die Herde weit besser, als einer festen Verzögerung einen kleinen Zufallswert beizumischen.

retry.py
# Ein Retry-Wrapper, den du wirklich produktiv schalten kannst. Zwei Regeln erledigen das meiste:
#   1. Wiederhole nur das Wiederholbare (429 + 5xx + Verbindungsfehler). Niemals 400/401/422.
#   2. Backe exponentiell ab UND füge Jitter hinzu, sonst wiederholen alle Clients im Gleichschritt.
import random
import time

from openai import OpenAI, APIStatusError, APIConnectionError, APITimeoutError

client = OpenAI(
    api_key="sk-brievio-...",
    base_url="https://api.brievio.com/v1",
    timeout=30,          # harte Obergrenze pro Request — siehe "Timeouts" weiter unten
)

RETRYABLE_STATUS = {429, 500, 502, 503, 504}
MAX_ATTEMPTS = 5
BASE_DELAY = 0.5         # Sekunden
MAX_DELAY = 20.0         # deckle das Backoff, damit eine langsame Erholung nicht ewig blockiert

def chat_with_retry(**kwargs):
    for attempt in range(MAX_ATTEMPTS):
        try:
            return client.chat.completions.create(**kwargs)
        except APIStatusError as e:
            # Ein 4xx, das nicht 429 ist, ist DEIN Bug (falsche Parameter, falscher Key, zu lang).
            # Das zu wiederholen verbrennt nur Latenz — scheitere schnell.
            if e.status_code not in RETRYABLE_STATUS:
                raise
            last_error = e
        except (APIConnectionError, APITimeoutError) as e:
            # Netzwerk-Aussetzer oder unser Timeout hat gefeuert. Ein Read darf wiederholt werden.
            last_error = e

        if attempt == MAX_ATTEMPTS - 1:
            break

        # Exponentielles Backoff mit FULL Jitter: sleep ∈ [0, base * 2**attempt].
        # Full Jitter (nicht "Backoff + kleiner Zufallswert") ist das, was eine
        # Thundering Herd tatsächlich entkoppelt. Siehe den AWS Architecture Blog zu jittered Backoff.
        ceiling = min(MAX_DELAY, BASE_DELAY * (2 ** attempt))
        time.sleep(random.uniform(0, ceiling))

    raise last_error

Beachte die Obergrenze für Versuche (MAX_ATTEMPTS = 5) und die Obergrenze für die Verzögerung (MAX_DELAY). Unbegrenzte Wiederholungen sind der Weg, auf dem aus einem vorübergehenden Aussetzer ein Backlog wird, der nie abfließt: Requests stauen sich schneller, als sie abgearbeitet werden, die Latenz klettert, und vorgelagerte Aufrufer laufen in Timeouts und wiederholen ihre Requests ebenfalls. Begrenze beides, und lass den Fehler sichtbar werden, statt ihn zu puffern.

Beachte Retry-After — rate nicht

Deine Backoff-Kurve ist eine Schätzung, wann wieder Kapazität frei wird. Wenn der Server dir die Antwort direkt nennt, nutze sie. Ein 429 bringt oft einen Retry-After-Header mit (Delta-Sekunden oder ein HTTP-Datum), der das echte Reset-Fenster widerspiegelt. Für diesen Wert zu schlafen ist strikt besser als jede Formel, weil es Grundwahrheit ist und keine Heuristik.

retry_after.py
# Wenn der Server dir sagt, wie lange du warten sollst, hör zu. Ein 429 (und manchmal
# ein 503) bringt einen Retry-After-Header mit. Ihn zu beachten schlägt jede Backoff-Kurve,
# die du dir ausdenkst, weil er das echte Reset-Fenster widerspiegelt — keine Schätzung.
import email.utils as eut
import time

def retry_delay(resp_headers, attempt, base=0.5, cap=20.0):
    # 1. Bevorzuge die Anweisung des Servers.
    ra = resp_headers.get("retry-after")
    if ra is not None:
        try:
            return float(ra)                       # Delta-Sekunden-Form: "2"
        except ValueError:
            when = eut.parsedate_to_datetime(ra)   # HTTP-Datums-Form
            return max(0.0, when.timestamp() - time.time())

    # 2. Kein Header? Falle zurück auf exponentielles Backoff mit Full Jitter.
    import random
    return random.uniform(0, min(cap, base * (2 ** attempt)))

# Hinweise:
#   - Manche Anbieter senden zusätzlich X-RateLimit-Reset; behandle es genauso.
#   - Füge einen winzigen Boden hinzu (z. B. 50 ms), damit ein "Retry-After: 0" nicht heißläuft.
#   - Brievio liefert Retry-After bei 429 aus, statt den Socket still hängen zu lassen,
#     dieser Pfad ist also erreichbar — du kannst tatsächlich auf ein Signal hin abbremsen.

Das funktioniert nur, wenn das Gateway den Header tatsächlich zurückgibt, statt das Limit zu schlucken und deinen Socket neunzig Sekunden lang hängen zu lassen. Brievio scheitert schnell und laut — ein Rate-Limit kommt als sauberes 429 mit Retry-After zurück, nicht als hängende Verbindung — sodass der Pfad „hör auf den Server“ erreichbar ist. Die vollständige Fehler-Taxonomie, samt welcher Code welche Header mitbringt, steht in der Fehlerreferenz.

Timeouts: der Fehlermodus, den niemand testet

Eine Retry-Policy ist nutzlos, wenn der Request nie zurückkehrt, um wiederholt zu werden. Ein KI-Call ohne Timeout wird irgendwann hängen — eine halb offene Verbindung, ein festsitzendes Upstream, ein Load Balancer, der den Flow verworfen hat. Ohne Deadline blockiert dieser eine Request einen Worker, eine Verbindung und einen Platz in jeder Queue dahinter, bis das OS Minuten später aufgibt. Setze ein explizites Timeout pro Request (das timeout=30 oben), damit ein festsitzender Call zu einem APITimeoutError wird, den du wiederholen kannst, statt zu totem Ballast.

  • Timeout pro Request begrenzt einen einzelnen Versuch. Beim Streaming ist das sinnvolle Budget die Zeit bis zum ersten Token plus ein Idle-Timeout zwischen den Chunks, nicht eine Wall-Clock-Gesamtzeit.
  • Gesamt-Deadline begrenzt die ganze Retry-Sequenz. Führe ein Budget (z. B. 60s Ende-zu-Ende) und hör auf zu wiederholen, sobald es aufgebraucht ist — ein Aufrufer, der auf dich wartet, hat seine eigene Deadline.
  • Timeout > erwartetes p99, nicht p50. Setze es knapp über deine echte Tail-Latenz. Zu eng, und du brichst gute Requests ab, die kurz vor dem Erfolg standen, und fabrizierst aus Ungeduld Last.

Idempotenz: Wiederholungen sicher machen

Wiederholungen bringen eine subtile Gefahr mit. Wenn ein Request ein Timeout hat, weißt du nicht, ob der Server ihn verarbeitet hat — dein Lesen der Antwort ist gescheitert, aber die Arbeit könnte abgeschlossen sein. Wiederhole blind, und du kannst einen Kunden doppelt belasten, eine Benachrichtigung zweimal senden oder eine doppelte Zeile schreiben. Reads lassen sich von Natur aus sicher wiederholen. Seiteneffekte nicht.

Die Verteidigung ist ein Idempotenz-Key: eine eindeutige ID, die du an eine logische Operation hängst, sodass der Server (oder dein eigener Handler) Duplikate zusammenfasst. Bei reinen Inferenz-Calls gibt es meist keinen externen Seiteneffekt, um den man sich sorgen müsste — aber sobald eine Completion einen DB-Schreibvorgang, eine Zahlung oder eine ausgehende Nachricht auslöst, erzeuge pro logischer Arbeitseinheit einen stabilen Key und dedupliziere darauf. Die Faustregel: Wenn eine Wiederholung zweimal passieren könnte, entwirf so, als würde sie es.

Circuit Breaker: hör auf, eine tote Dependency zu treten

Backoff behandelt einen einzelnen strauchelnden Request. Es bringt nichts bei einem anhaltenden Ausfall — wenn ein Upstream zwei Minuten lang hart down ist, läuft jeder Request seine komplette Retry-Leiter, wartet das Maximum und scheitert trotzdem, während deine Latenz und Queue-Tiefe explodieren. Ein Circuit Breaker schließt das kurz: Nach N aufeinanderfolgenden Fehlern „öffnet“ er und lässt neue Calls sofort scheitern (oder leitet sie auf einen Fallback) für ein Abkühlfenster, dann lässt er eine einzelne Probe durch, um die Erholung zu testen, bevor er wieder schließt.

  • Geschlossen: Normalbetrieb, Requests fließen, Fehler werden gezählt.
  • Offen: Schwelle überschritten — schnell ablehnen für eine Abkühlphase, statt zum Scheitern verurteilte Wiederholungen aufzustapeln.
  • Halb offen: nach der Abkühlung einen Testrequest zulassen; Erfolg schließt den Breaker, ein Fehler öffnet ihn wieder.

Kombiniere den Breaker mit einem Fallback-Pfad, und der Ausfall wird zu einer Degradation statt eines harten Scheiterns. Genau hier verdient sich auch ein Gateway seinen Lohn: Brievio macht anbieterübergreifendes Failover, sodass ein einzelner Anbieter, der ausfällt, auf einen gesunden umgeleitet werden kann, bevor dein Breaker überhaupt öffnen muss. Wie das in ein echtes Zuverlässigkeitsbudget passt, siehst du in wie wir ein 99,95-%-SLO konstruieren.

Eine Anmerkung dazu, was Wiederholungen kosten

Aggressives Retry-Tuning macht Leute nervös wegen der Rechnung — jede Wiederholung ist ein weiterer abrechenbarer Call, oder? Nicht auf einem Gateway, das ehrlich abrechnet. Brievio berechnet nichts für fehlgeschlagene 4xx/5xx-Calls, also sind das 429, das dein Backoff ausgelöst hat, und das 503, an dem du vorbei wiederholt hast, kostenlos. Du zahlst für den Versuch, der tatsächlich eine Completion zurückgibt, nicht für die, die das System abgewiesen hat. Das heißt, du kannst Versuchszahlen und Timeouts auf Zuverlässigkeit trimmen, ohne dafür mit einem Zähler bestraft zu werden — und falls außer Kontrolle geratene Wiederholungen trotzdem eine Budget-Sorge sind, deckle die Ausgaben direkt, wie beschrieben in wie du deine KI-API-Ausgaben deckelst.

Das Fazit

Produktionsreife Fehlerbehandlung für KI-APIs sind sechs Regeln, und du kannst sie alle an einem Nachmittag produktiv schalten:

  • Wiederhole nur 429 und 5xx. Wiederhole nie anderes 4xx — das ist dein Bug, sichtbar gemacht.
  • Backe exponentiell ab mit Full Jitter, und deckle sowohl Versuche als auch Verzögerung.
  • Beachte Retry-After, wenn der Server es sendet — es schlägt jede Formel.
  • Setze explizite Timeouts pro Request und eine Gesamt-Deadline; lass einen Call nie hängen.
  • Mach seiteneffektbehaftete Wiederholungen mit einem Idempotenz-Key sicher.
  • Füge einen Circuit Breaker hinzu, sodass ein anhaltender Ausfall degradiert, statt zu kollabieren.

Nichts davon ist exotisch — es ist die langweilige Infrastruktur, die das langweilige Versprechen hält, oben zu bleiben. Sie funktioniert auch am besten auf einer Basisschicht, die schnell scheitert, echte Signale sichtbar macht und dir die Fehler nicht in Rechnung stellt, sodass dein Tuning ehrlich und kostenlos ist. Wenn du noch wählst, wohin du deinen Traffic lenkst, ist das Zuverlässigkeitsverhalten unter Fehlern eines der Dinge, die es sich zuerst zu testen lohnt — behandelt in wie du ein KI-API-Gateway auswählst.