Drei Uhr morgens, PagerDuty klingelt. Der PaymentService ist down. Passiert, passiert öfter als man denkt. Aber dann schaust du auf die Metriken und etwas stimmt nicht: der Service bekommt nicht die erwarteten 200 req/s. Er bekommt 5.400. Fünftausenvierhundert. Alle 503.
Du schaust in die Config. Retries: 3, mit Exponential Backoff. Sieht vernünftig aus. Jeder Service hat das — Gateway, OrderService, PaymentService. Best Practice, oder?
Rechne mal: 3 × 3 × 3 = 27.
Ein einziger fehlgeschlagener User-Request erzeugt 27 Requests am PaymentService. Kein Angriff. Kein Bug. Deine Retry-Config ist der Angriff.
Kill den PaymentService und zähl mit:
Siehst du die Multiplikation? Der Gateway schickt 3 Requests. Jeder triggert 3 am OrderService. Jeder davon triggert 3 am PaymentService. 27 Requests. Schalte jetzt auf Retry Budget um und sieh den Unterschied: 4 statt 27.
Warum Backoff nicht hilft
“Aber wir haben doch Exponential Backoff!” Backoff verlangsamt die Retries. Das hilft, wenn das Problem vorübergehend ist — kurzer Netzwerk-Hickup, GC-Pause. Aber Backoff ändert nichts an der Anzahl: du schickst trotzdem 27 Requests. Nur langsamer.
Und wenn der Service wirklich down ist, nicht nur kurz weg? Dann retried jedes Tier für Sekunden. Das Gateway wartet auf den OrderService der wartet auf den PaymentService der nicht antwortet. Timeouts addieren sich. Dein User sieht einen 30-Sekunden-Spinner, und am Ende einen Fehler.
Das Problem hat einen Namen: Retry Amplification. Jede Schicht im Call Stack multipliziert die Last auf die darunterliegende. In einem System mit 5 Tiers und 3 Retries pro Tier: 3^5 = 243 Requests. Für einen einzigen User-Click.
Retry Budgets
Die Idee ist simpel: statt einer festen Anzahl Retries pro Request trackst du den Anteil deiner Requests die Retries sind. Wenn mehr als 20% deiner ausgehenden Requests Retries sind, hörst du auf zu retrien.
class RetryBudget {
private total = 0;
private retries = 0;
private readonly windowMs = 10_000;
private readonly maxRetryRatio = 0.2;
private timestamps: number[] = [];
private retryTimestamps: number[] = [];
record(isRetry: boolean) {
const now = Date.now();
this.timestamps.push(now);
if (isRetry) this.retryTimestamps.push(now);
this.cleanup(now);
}
canRetry(): boolean {
const now = Date.now();
this.cleanup(now);
if (this.timestamps.length < 10) return true; // zu wenig Daten
return (this.retryTimestamps.length / this.timestamps.length) < this.maxRetryRatio;
}
private cleanup(now: number) {
const cutoff = now - this.windowMs;
this.timestamps = this.timestamps.filter(t => t > cutoff);
this.retryTimestamps = this.retryTimestamps.filter(t => t > cutoff);
}
}
Wenn der PaymentService down ist und 100% der Requests scheitern, wird das Budget nach wenigen Requests erreicht. Der OrderService hört auf zu retrien. Die Kaskade bricht ab.
Google beschreibt Retry Budgets in ihrem SRE Book als Standard-Pattern. Die Faustregel dort: maximal 10% Retry-Anteil im Normalbetrieb, und nie mehr als 3 Retries pro Request zusätzlich zum Budget.
Jitter
Wenn 50 Requests gleichzeitig scheitern und alle nach exakt 1 Sekunde retrien — was passiert? Alle 50 treffen gleichzeitig ein. Wieder Fehler. Wieder 1 Sekunde warten. Wieder 50. Das ist kein Backoff, das ist eine Welle.
Jitter löst das: statt nach genau baseDelay * 2^attempt zu warten, addierst du eine zufällige Komponente:
function backoffWithJitter(attempt: number, baseMs = 100): number {
const exponential = baseMs * Math.pow(2, attempt);
return Math.random() * exponential; // Full Jitter
}
Full Jitter streut die Retries gleichmäßig über das Zeitfenster. Statt einer Welle bekommst du ein Rauschen. Die Wahrscheinlichkeit, dass alle gleichzeitig retrien, geht gegen null.
Die goldene Regel
Je näher am User, desto mehr darfst du retrien. Je tiefer in der Kette, desto weniger.
Dein API Gateway darf 2-3 mal retrien, der User wartet sowieso auf eine Antwort. Aber der interne Service drei Ebenen tief? Der sollte einmal versuchen und bei Fehler sofort aufgeben. Warum? Weil die Ebene darüber bereits retried. Wenn jede Ebene nur einmal retried statt dreimal: 2 × 2 × 2 = 8 statt 3 × 3 × 3 = 27. Und mit Budget bist du bei 4.
Kombinier das mit einem Circuit Breaker auf jeder Ebene. Wenn der PaymentService drei Sekunden nicht antwortet, öffnet der Breaker. Keine Requests mehr. Keine Retries. Die Kaskade stirbt sofort statt über 30 Sekunden auszubluten.
Retries sind kein Set-and-Forget. Sie sind eine Waffe die in beide Richtungen feuert.