Um 2:41 Uhr hat mein Handy vibriert, und ich habe zehn Sekunden gebraucht, um zu verstehen, wo ich bin. Auf dem Bildschirm stand ein Graph, der wie eine Wand aussah: Requests pro Minute von 400 auf 6.000, in vier Minuten. Ich war seit sechs Wochen im Praktikum, hatte die Pikett-Nummer und keine Ahnung, was ich tun sollte. Der erste Gedanke war: Skalieren. Der zweite, drei Minuten später: Warte, das sind wir selbst.

Was ein Retry ohne Pause anrichtet

Ein Retry ist die freundlichste Art, ein kaputtes System zu treten. Wenn ein Aufruf fehlschlägt und der Client ihn sofort wiederholt, verdoppelt sich die Last auf einem System, das gerade schon nicht antworten konnte. Wenn 200 Clients das gleichzeitig tun, hast du nicht 200 Probleme, du hast ein Problem, das 200-mal so groß ist. Und wenn sie alle in derselben Sekunde anfangen, weil sie alle denselben Timer benutzen, hast du zusätzlich einen Rhythmus – eine Lastwelle, die immer dann kommt, wenn das System gerade Luft holt.

Das ist der Punkt, an dem ich aufgehört habe, Retries für eine Robustheitsmaßnahme zu halten. Ein Retry ohne Pause und ohne Obergrenze ist ein Lasttest, den du versehentlich gegen dich selbst fährst.

Die Rechnung dahinter ist einfach und brutal. Ein Client, der sofort wiederholt, erzeugt bei jedem Fehler das Doppelte bis Fünffache an Anfragen. Zwei Clients, die das tun, erzeugen vier- bis fünfundzwanzigfache. Das ist kein lineares Problem, das ist eine Kurve, die aussieht wie eine Wand, und die Wand kommt nicht, weil mehr Menschen die App benutzen, sondern weil dieselben Menschen dieselbe Anfrage noch einmal schicken.

Der Vorfall, den wir selbst gebaut haben

Monitoring-Graph mit steiler Lastkurve um drei Uhr nachts
2:41 Uhr: 400 auf 6.000 Requests pro Minute, in vier Minuten.

Der Kunde war nicht böse. Er hatte unsere Empfehlung umgesetzt: „Bei Fehlern bis zu fünfmal wiederholen.“ Was in der Dokumentation nicht stand: mit welchem Abstand, mit welcher Obergrenze und was passiert, wenn der Fehler ein 429 ist. Sein Client hat bei jedem Fehler sofort wiederholt, fünfmal, ohne Pause, und weil er viele Instanzen hatte, hat er sich selbst multipliziert. Wir haben um 3:10 die Rate Limits verschärft, um 3:25 war der Graph flach, und ich habe bis 5 Uhr dagesessen und zugeschaut, ob es hält.

Was mich daran bis heute ärgert: Unser eigener Client hatte denselben Fehler. Wir haben ihn nur nie ausgelöst, weil unsere Last klein war. Die Regel, die wir anderen gegeben haben, hätten wir selbst nicht überlebt.

Was wir in der Nacht nicht gesehen haben, war der Zusammenhang. Auf unserem Dashboard stand „Fehlerrate steigt“, und daneben stand „Requests steigen“, und beides sah aus wie eine Ursache und eine Wirkung. In Wahrheit war es umgekehrt: Die Fehlerrate ist gestiegen, weil ein Anbieter langsam war, und die Requests sind gestiegen, weil unsere Clients auf die Langsamkeit reagiert haben. Diese Verwechslung hat uns zwanzig Minuten gekostet, in denen wir bei uns selbst gesucht haben.

Wie Backoff richtig aussieht

Drei Zutaten, und alle drei sind wichtig. Exponentielles Wachstum, damit der Abstand schnell groß wird. Eine Obergrenze, damit er nicht ins Unendliche läuft. Und Jitter, also eine zufällige Verschiebung, damit nicht alle Clients im Gleichtakt anklopfen. Der Jitter ist der Teil, den fast alle weglassen, und es ist der, der den Unterschied macht, wenn viele Clients gleichzeitig betroffen sind.

delay := base
for versuch := 1; versuch <= maxVersuche; versuch++ {
    if err := call(); err == nil {
        return nil
    }
    // Jitter: ohne Zufall klopfen alle im Gleichtakt
    schlaf := delay/2 + rand.N(delay/2)
    time.Sleep(schlaf)
    delay *= 2
    if delay > maxDelay {
        delay = maxDelay
    }
}

Dazu gehört eine Regel, die nichts mit Zeit zu tun hat: Nicht jeder Fehler darf wiederholt werden. Ein 400 wird durch Wiederholen nicht besser. Ein 429 sagt dir sogar, wie lange du warten sollst. Und ein POST ohne Idempotenz-Schlüssel zweimal zu schicken kann eine doppelte Bestellung erzeugen – dann hast du das System gerettet und dem Kunden zweimal dasselbe Buch verkauft.

Was ich in dieser Nacht gelernt habe

Erstens: Retries brauchen ein Budget. Nicht „fünfmal“, sondern „so viele, dass die Gesamtlast nicht steigt“. Zweitens: Ein Circuit Breaker ist keine Schikane, sondern Höflichkeit – wenn ein Dienst tot ist, hör auf, ihn zu fragen. Drittens: Schreib in die Dokumentation nicht nur, was der Client tun soll, sondern auch, was er nicht tun soll. Wir hatten Ersteres und nicht Letzteres, und das hat uns die Nacht gekostet.

Und die unangenehmste Erkenntnis: Ich habe um 2:44 Uhr kurz darüber nachgedacht, die Instanzen zu verdoppeln. Das hätte funktioniert, für ungefähr vier Minuten. Danach hätten wir doppelt so viele Instanzen gehabt, die doppelt so viele Retries geschickt hätten.

Was ich außerdem gelernt habe: Ein Retry-Budget ist eine Zahl, die man aufschreiben kann. „Wir wiederholen höchstens so oft, dass die zusätzliche Last unter zehn Prozent bleibt.“ Es ist keine exakte Zahl, aber eine Grenze, über die man streiten kann. Bei uns steht sie jetzt im Runbook, mit dem Satz daneben: Wenn du sie überschreitest, hast du kein Retry-Problem, sondern ein Kapazitätsproblem.

Fazit

Retries sind wie Schmerzmittel: nützlich in der richtigen Dosis, gefährlich, wenn man sie nach Gefühl nimmt. Ich schreibe seitdem in jeden Client zuerst den Backoff und dann den Aufruf, weil die Reihenfolge im Kopf dieselbe ist: wie oft, wie schnell, und was passiert, wenn alle es gleichzeitig tun.

Wie oft wiederholt dein Client einen fehlgeschlagenen Aufruf – und weißt du, was passiert, wenn das alle gleichzeitig tun?