Zruck zum Blog, du Zniachtl
Feb. 2026·3 Min. zum LesnVo da KI gmocht

Des Circuit-Breaker-Muster für belastbare Services

#backend#reliability#distributed-systems

Wenn a nachgelagerter Service zu scheitern anfangt, is die naive Reaktion - jeden Request, wia er einekummt, weiter wiederholn - genau der falsche Zug. Jede Wiederholung is a weiterer Request, der si auf an Service draufpackt, der eh scho kämpft, und jeder Aufrufer, der auf a Timeout wartet, is a Thread oda a Verbindung, de dein eigener Service für nix ondres nutzen kann. Des Circuit-Breaker-Muster gibt's, um des zu stoppen: Nach genug Fehlschlägen hört er für a Weile gänzlich auf, Requests zu schickn, und scheitert schnell statt langsam.

De drei Zustände

A Circuit Breaker is a State Machine mit drei Zuständen, benannt nach da elektrischen Analogie:

  • Closed - Normalbetrieb. Requests gehn zum nachgelagerten Service durch. Fehlschläge wern gezählt.
  • Open - die Fehlerschwelle wurde überschritten. Requests scheitern sofort, ohne den Aufruf überhaupt zu versuchen, für a Abkühlphase.
  • Half-open - nach da Abkühlphase wern oa (oda a poar) Test-Requests durchglassn. Wenn sie klappen, schließt da Breaker wieda. Wenn sie scheitern, öffnet er neu und die Abkühlphase fangt von vorn o.
CLOSED --(too many failures)--> OPEN --(cooldown elapses)--> HALF_OPEN
   ^                                                              |
   |______________________(test request succeeds)_________________|
                            (test request fails) -----> OPEN

Die Schlüsseleinsicht liegt im "Open"-Zustand: Statt dass jeder Aufrufer einzeln auf a Timeout gegn an Service wartet, der down is, scheitert da Breaker sofort und billig, gibt auf deiner Seitn Ressourcen frei und lasst dem nachgelagerten Service Luft zum Erholen, ohne dass no mehr Last draufkummt.

A minimale Implementierung

class CircuitBreaker {
  constructor(fn, { failureThreshold = 5, cooldownMs = 30_000 } = {}) {
    this.fn = fn;
    this.failureThreshold = failureThreshold;
    this.cooldownMs = cooldownMs;
    this.state = 'CLOSED';
    this.failureCount = 0;
    this.openedAt = null;
  }

  async call(...args) {
    if (this.state === 'OPEN') {
      if (Date.now() - this.openedAt >= this.cooldownMs) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit is open — failing fast');
      }
    }

    try {
      const result = await this.fn(...args);
      this.#onSuccess();
      return result;
    } catch (err) {
      this.#onFailure();
      throw err;
    }
  }

  #onSuccess() {
    this.failureCount = 0;
    this.state = 'CLOSED';
  }

  #onFailure() {
    this.failureCount++;
    if (this.state === 'HALF_OPEN' || this.failureCount >= this.failureThreshold) {
      this.state = 'OPEN';
      this.openedAt = Date.now();
    }
  }
}

Verwendung - wickel jede Funktion ein, de an riskanten Aufruf macht:

const breaker = new CircuitBreaker(
  (orderId) => fetch(`https://payments.internal/charge/${orderId}`).then((r) => r.json()),
  { failureThreshold: 5, cooldownMs: 30_000 },
);

try {
  const result = await breaker.call(order.id);
} catch (err) {
  // Either the payment call actually failed, or the breaker is open —
  // either way, don't retry synchronously in the same request.
  return res.status(503).json({ error: 'Payment service unavailable, try again shortly' });
}

Beachte, dass a einziger fehlgschlogener Aufruf in HALF_OPEN den Breaker sofort wieda öffnet, statt die volle Schwelle nochamoi zu verlangen - der ganze Sinn vom Half-open-Probeaufruf is Vorsicht, drum soit oa schlechtes Testergebnis reichen, um zu schließen, dass der Service no ned bereit is.

Circuit Breaker vs. Retry vs. Timeout

De drei wern oft verwechselt, weil sie olle auf "der nachgelagerte Aufruf funktioniert ned" reagiern, aber sie lösn unterschiedliche Probleme:

  • Timeout begrenzt, wie lang du auf an einzelnen Aufruf wartest, bevor du aufgibst.
  • Retry wiederholt an einzelnen fehlgschlogenen Aufruf, meist mit Backoff, in da Annahme, dass der Fehler vorübergehend woar.
  • Circuit Breaker trackt Fehlschläge über Aufrufe hinweg über die Zeit und hört komplett auf, Aufrufe zu versuchen, sobois a Service systemisch kaputt ausschaut statt nur vorübergehend vom Pech verfolgt.

Sie lassen si kombiniern: A Aufruf wird typischerweise in a Timeout eingwickelt, bei Fehlern a begrenzte Anzoi Moi wiederholt, und die Retry-Logik söba sitzt hinter am Circuit Breaker, damit a systemisch ausgfallener Service ned mit (Timeout + Wiederholungen) × jedem eingehenden Request bombardiert wird.

Wos die Fehlerschwelle zählen soit

Rohe Fehlschläge über unbegrenzte Zeit zu zählen is meist falsch - fünf Fehlschläge bei fünf Requests in dieser Minute sand a echtes Problem; fünf Fehlschläge bei zehntausend Requests über a Woch sand Rauschen. A Sliding Window (Fehlschläge in den letztn N Requests oda den letztn N Sekunden zähln) is die praktische Wahl:

class SlidingWindowBreaker {
  constructor(fn, { failureRateThreshold = 0.5, windowSize = 20, cooldownMs = 30_000 } = {}) {
    this.fn = fn;
    this.failureRateThreshold = failureRateThreshold;
    this.windowSize = windowSize;
    this.cooldownMs = cooldownMs;
    this.state = 'CLOSED';
    this.results = []; // rolling window of true/false (success/failure)
    this.openedAt = null;
  }

  #recordResult(success) {
    this.results.push(success);
    if (this.results.length > this.windowSize) this.results.shift();
  }

  #failureRate() {
    if (this.results.length < this.windowSize) return 0; // not enough data yet
    const failures = this.results.filter((r) => !r).length;
    return failures / this.results.length;
  }

  async call(...args) {
    if (this.state === 'OPEN') {
      if (Date.now() - this.openedAt < this.cooldownMs) {
        throw new Error('Circuit is open — failing fast');
      }
      this.state = 'HALF_OPEN';
    }

    try {
      const result = await this.fn(...args);
      this.#recordResult(true);
      if (this.state === 'HALF_OPEN') this.state = 'CLOSED';
      return result;
    } catch (err) {
      this.#recordResult(false);
      if (this.state === 'HALF_OPEN' || this.#failureRate() >= this.failureRateThreshold) {
        this.state = 'OPEN';
        this.openedAt = Date.now();
      }
      throw err;
    }
  }
}

Wo ma den Breaker hinstöt

Der Breaker soit pro nachgelagerter Abhängigkeit existiern, ned global - a ausfallender Empfehlungs-Service soit ned an Breaker auslösn, der a die Aufrufe zu deim Zahlungsanbieter sperrt. In ana Service mit mehreren externen Abhängigkeiten heißt des, oane Breaker-Instanz pro Abhängigkeit, jede mit Schwellen, de auf die tatsächlichen Ausfalleigenschaften dieser Abhängigkeit abgstimmt sand (a wackelige Drittanbieter-API kunnt a niedrigere Schwelle und längere Abkühlphase rechtfertigen ois a interner Service im söbn Netz).

Fazit

Der Job von am Circuit Breaker is ned, Ausfälle zu verhindern - er soll verhindern, dass dein eigener Service an nachgelagerten Ausfall schlimmer macht, indem er ihn mit Last bombardiert, de er ned bewältigen kann, und dass dein eigener Service seine Ressourcen ned damit vergeudet, auf Aufrufe zu warten, de sehr wahrscheinlich eh scheitern. Die State Machine is einfach; der Teil, der richtig sei soit, is die Wahl von Schwellen und Fenstergrößen, de dazu passen, wia die Abhängigkeit in da Praxis tatsächlich ausfällt - ned einfach runde Zahlen zum nehmen.

NeicherOptimistisches vs. pessimistisches Locking in DatenbankenElterFeature Flags: A praktischer Leitfodn