Zurück zum Blog
Mai 2026·3 Min. LesezeitKI-generiert
Sprache:DeutschEnglishFrançaisÖsterreichischKlingonisch Originalfassung: English

Retry-Strategien: Exponentielles Backoff und Jitter

#backend#reliability#networking

Ein Netzwerkaufruf schlägt fehl. Die naheliegende Lösung ist „einfach wiederholen" – aber naives Wiederholen ist genau der Weg, wie ein kurzer Aussetzer zu einem vollständigen Ausfall wird, weil jeder Client den sich erholenden Dienst im exakt gleichen Moment bombardiert. Exponentielles Backoff mit Jitter ist die Standardlösung, und sie lässt sich in etwa fünfzehn Zeilen implementieren, sobald man versteht, warum jedes Teil da ist.

Warum naive Wiederholungen alles schlimmer machen

Angenommen, ein nachgelagerter Dienst fällt unter Last für zwei Sekunden aus. Tausend Clients bekommen eine fehlgeschlagene Anfrage, und alle wiederholen sofort. Der Dienst, der gerade erst wieder hochkommt, bekommt dieselben tausend Anfragen noch einmal auf einen Schlag – plus den Traffic, der in der Zwischenzeit neu eingetroffen ist. Er fällt wieder um. Das ist ein Retry Storm, und es ist ein häufiger Weg, wie aus einem kleinen, vorübergehenden Aussetzer ein langer Ausfall wird.

Zwei Probleme müssen behoben werden: Wiederholungen, die zu früh passieren, und Wiederholungen, die bei allen Clients im Gleichschritt passieren.

Exponentielles Backoff: Wiederholungen auseinanderziehen

Statt sofort zu wiederholen, wartet man nach jedem Fehlschlag länger – typischerweise verdoppelt sich die Wartezeit jedes Mal.

async function withBackoff(fn, { maxRetries = 5, baseDelayMs = 200, maxDelayMs = 10000 } = {}) {
  let attempt = 0;

  while (true) {
    try {
      return await fn();
    } catch (err) {
      attempt++;
      if (attempt > maxRetries) throw err;

      const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), maxDelayMs);
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
}

Mit baseDelayMs: 200 betragen die Wartezeiten etwa 200 ms, 400 ms, 800 ms, 1600 ms, 3200 ms – sie wachsen schnell, begrenzt durch maxDelayMs, damit eine wacklige Abhängigkeit eine Anfrage nicht minutenlang warten lässt. Das allein behebt „zu früh wiederholen". Gegen „im Gleichschritt wiederholen" hilft es nichts, weil jeder Client genau dieselbe Abfolge von Wartezeiten berechnet.

Jitter: den Gleichschritt aufbrechen

Wenn jeder Client genau 200 ms zurückweicht, dann genau 400 ms, wiederholen sie relativ zueinander immer noch im selben Augenblick – nur seltener. Jitter fügt der Wartezeit Zufall hinzu, damit sich die Wiederholungen über die Zeit verteilen, statt in synchronisierten Wellen einzutreffen.

function backoffWithFullJitter(attempt, baseDelayMs, maxDelayMs) {
  const cap = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
  return Math.random() * cap;
}

Das ist „Full Jitter" – die Wartezeit ist ein Zufallswert zwischen 0 und der exponentiellen Obergrenze, nicht die Obergrenze selbst. Der Architektur-Blog von AWS hat diesen Ansatz populär gemacht, nachdem er sowohl schlichtes exponentielles Backoff als auch „Equal Jitter" (Obergrenze/2 plus ein zufälliger Anteil von Obergrenze/2) unter hoher Konkurrenz übertraf, genau weil er Wiederholungen über das breiteste Fenster verteilt.

So bindet man ihn in die Retry-Schleife ein:

async function withBackoffAndJitter(fn, { maxRetries = 5, baseDelayMs = 200, maxDelayMs = 10000 } = {}) {
  let attempt = 0;

  while (true) {
    try {
      return await fn();
    } catch (err) {
      attempt++;
      if (attempt > maxRetries) throw err;

      const delay = backoffWithFullJitter(attempt - 1, baseDelayMs, maxDelayMs);
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
}

Nicht jeder Fehler verdient eine Wiederholung

Blind bei jedem Fehler zu wiederholen ist ein eigener Bug. Ein 400 Bad Request oder 401 Unauthorized schlägt immer identisch fehl, egal wie oft man es wiederholt – man fügt nur nutzlos Last hinzu. Wiederhole nur Fehler, die plausibel vorübergehend sind:

  • Fehler auf Netzwerkebene (Verbindung zurückgesetzt, Timeout)
  • 429 Too Many Requests
  • 502, 503, 504 – Gateway-/Upstream-Fehler
function isRetryable(err) {
  if (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT') return true;
  if (err.status && [429, 502, 503, 504].includes(err.status)) return true;
  return false;
}

Prüfe das, bevor du zurückweichst, und wirf bei allem anderen sofort erneut:

} catch (err) {
  if (!isRetryable(err)) throw err;
  // ...backoff logic
}

Retry-After beachten

Eine 429- oder 503-Antwort kommt oft mit einem Retry-After-Header, der dir genau sagt, wie lange du warten sollst. Ist er vorhanden, nutze ihn statt deiner selbst berechneten Wartezeit – der Server kennt seine eigene Erholungszeit besser als deine Schätzung.

function delayFromResponse(err, fallbackDelayMs) {
  const retryAfter = err.headers?.['retry-after'];
  if (!retryAfter) return fallbackDelayMs;

  const seconds = Number(retryAfter);
  return Number.isFinite(seconds) ? seconds * 1000 : fallbackDelayMs;
}

Das gesamte Retry-Budget begrenzen, nicht nur die Wartezeit

maxRetries allein kann eine einzelne logische Operation immer noch lange dauern lassen, wenn die Obergrenzen großzügig sind – fünf Wiederholungen mit 10 Sekunden maximaler Wartezeit können potenziell über 30 Sekunden für einen einzigen Aufruf verbrauchen. Hat der Aufrufer ein eigenes Timeout (ein Nutzer, der auf eine Anfrage wartet, ein Queue-Konsument mit Visibility-Timeout), verfolge die verstrichene Zeit und brich früh ab:

async function withDeadline(fn, deadlineMs) {
  const start = Date.now();
  return withBackoffAndJitter(async () => {
    if (Date.now() - start > deadlineMs) {
      throw new Error('retry deadline exceeded');
    }
    return fn();
  });
}

Fazit

Exponentielles Backoff behebt Wiederholungen, die zu früh passieren. Jitter behebt Wiederholungen, die bei allen Clients synchron passieren. Keines ersetzt die Prüfung, ob ein Fehler überhaupt eine Wiederholung wert ist, und keines sollte einen Retry-After-Header ignorieren, wenn der Server sich die Mühe macht, ihn zu senden. Die meisten HTTP-Client-Bibliotheken (axios-Interceptors, die Retry-Option von got, Resilienz-Bibliotheken wie cockatiel oder polly in .NET) implementieren all das bereits – greife in Produktionscode zuerst zu einer davon, statt es von Hand zu bauen, aber wisse, was sie unter der Haube tut, damit du erkennst, wann die Standardwerte nicht zu deiner Situation passen.

NeuerDatenbanktransaktionen und Isolationsstufen erklärtÄlterEine sanfte Einführung in WebSockets