Naar de inhoud
NLEN
Illustratie: Circuit breakers voor instabiele LLM-APIs

Circuit breakers implementeren voor instabiele LLM-APIs

Door Ivo Donker — samengesteld met AI-ondersteuning (Claude & Gemini)

Wanneer een traditionele REST-API faalt, geeft deze doorgaans binnen enkele milliseconden een duidelijke HTTP 500-statuscode terug. Bij Large Language Model (LLM) endpoints verloopt een storing fundamenteel anders. LLM-aanroepen zijn computationeel zwaar, duren van nature meerdere seconden en vertonen bij overbelasting geen directe crash, maar exponentieel oplopende wachttijden, vastlopende streams en periodieke time-outpieken. Als honderden gelijktijdige threads blijven wachten op een stagnerende upstream-provider, raken interne applicatiepools, worker-processen en socketverbindingen binnen enkele seconden volledig uitgeput.

Om te voorkomen dat haperende externe AI-diensten je complete backend-infrastructuur meesleuren in een kettingreactie van uitval, is een robuust isolatiemechanisme vereist. In dit artikel behandelen we het ontwerpen en implementeren van een gespecialiseerd circuit breaker-patroon dat is afgestemd op de asymmetrische latency en het faalgedrag van moderne LLM-integraties. Voor een breder fundament over betrouwbare architectuurstructuren verwijzen we naar het overzicht over robuuste integraties bouwen, waarin de wisselwerking tussen retries, deadlines en circuitisolatie op systeemniveau wordt toegelicht.

Waarom traditionele circuit breakers tekortschieten bij LLMs

Het klassieke circuit breaker-patroon, bekend uit gedistribueerde microservices, telt het aantal opeenvolgende mislukte netwerkverzoeken. Zodra een vooraf ingestelde drempelwaarde wordt overschreden, springt het circuit open en worden volgende verzoeken direct lokaal afgewezen om het onderliggende systeem te ontzien. Hoewel deze logica effectief is voor snelle RPC-aanroepen, faalt de standaardconfiguratie bij LLM-gateways door drie specifieke eigenschappen van taalmodellen:

Wanneer een upstream-provider degradeert en de responstijd van 2 seconden naar 30 seconden stijgt, blijven binnenkomende verzoeken zich opstapelen. Zonder circuit breaker blijven worker threads geblokkeerd totdat de harde time-out verstrijkt. Dit leidt tot geheugenuitputting in je API-gateway en maakt applicaties onbereikbaar voor eindgebruikers, zelfs voor functionaliteiten die geen AI vereisen.

De drie toestanden: Closed, Open en Half-Open in een LLM-context

De toestandsmachine van een circuit breaker bestaat uit drie hoofdfasen. Om effectief te functioneren met LLM-endpoints, moeten de overgangen tussen deze statussen rekening houden met de dynamiek van token-generatie en wachttijden.

Toestandslogica:
Closed (Normaal): Verzoeken worden direct doorgestuurd naar de primaire provider. Fouten en trage latency's worden geregistreerd in een glijdend venster.
Open (Geactiveerd): Verzoeken worden direct lokaal onderschept zonder netwerkverkeer naar de provider. De gateway schakelt over naar een lokale fallback of een alternatieve provider.
Half-Open (Testfase): Na een afkoelperiode (cool-down timer) laat het systeem een beperkt percentage van het verkeer (canary requests) door om te testen of de provider is hersteld.

Het kritieke verschil bij LLM's zit in de Half-Open status. Omdat een enkele modelaanroep meerdere seconden kan duren, mag een circuit breaker in de Half-Open status nooit direct tientallen parallelle verzoeken doorlaten. Eén enkele zware prompt kan de provider opnieuw overbelasten. In plaats daarvan moet een strikte probe-concurrency worden gehanteerd: slechts 1 of 2 verzoeken tegelijk krijgen toegang, waarbij strikt wordt gekeken naar zowel de HTTP-statuscode als de Time-To-First-Token (TTFT).

Foutcriteria en metric tracking voor LLM-aanroepen

Niet elke mislukte API-aanroep mag meetellen voor het activeren van de circuit breaker. Het categoriseren van fouten voorkomt dat legitieme foutieve invoer van gebruikers het circuit onnodig activeert.

HTTP Status / Situatie Telt als Circuit-fout? Toelichting en Actie
500, 502, 503, 504 Ja Interne serverfouten of gateway-problemen bij de provider duiden op infrastructurele instabiliteit.
Time-out (TTFT > drempel) Ja Provider reageert te traag; worker threads dreigen uitgeput te raken.
Stream drop / JSON corruptie Ja Verbinding verbreekt halverwege de payloadgeneratie; wijst op overbelasting van de inference cluster.
429 Too Many Requests Conditioneel Telt alleen mee als de retry-after drempel de maximale wachttijd overschrijdt.
400 Bad Request / 422 Unprocessable Nee Fout ligt bij de client (bijv. ongeldige parameters of contextlengte overschreden); provider functioneert normaal.
401 Unauthorized / 403 Forbidden Nee (Direct alarm) Configuratiefout in sleutelbeheer; circuit breaker lost dit niet op, vereist directe operationele notificatie.

Wanneer een verzoek strandt door netwerkfouten, is het belangrijk om herhalingen zorgvuldig te reguleren. Raadpleeg het artikel over retries en exponentiële backoff om te zien hoe individuele retry-mechanismen moeten samenwerken met de overkoepelende status van de circuit breaker, zodat een falende provider niet wordt gebombardeerd met verzoeken.

Sliding window algoritmes: Count-based versus Time-based

Om te bepalen of het circuit moet trippen, evalueert de breaker de verzoeken binnen een glijdend venster (sliding window). Er zijn twee dominante benaderingen: count-based en time-based.

Count-based sliding window

Bij een count-based venster analyseert het systeem de laatste N verzoeken (bijvoorbeeld de laatste 100 calls). Als meer dan een bepaald percentage (bijvoorbeeld 50%) faalt of de deadline overschrijdt, opent het circuit. Dit mechanisme werkt uitstekend bij continue, stabiele verkeersstromen, maar reageert traag bij lage verkeersvolumes buiten kantooruren.

Time-based sliding window

Een time-based venster meet de prestaties over de afgelopen T seconden (bijvoorbeeld de laatste 60 seconden), onderverdeeld in kleinere buckets van 5 of 10 seconden. Voor LLM-gateways verdient de time-based benadering met een minimale volume-drempel de voorkeur. Omdat LLM-uitval zich vaak plotseling manifesteert tijdens piekuren, zorgt een tijdvenster van 30 tot 60 seconden voor een snelle respons zonder dat oude historische fouten de huidige status blijven beïnvloeden.

Daarnaast is het essentieel om strakke deadline-budgetten te hanteren binnen elk venster. In het artikel over timeouts en annulering wordt uitgelegd hoe je met AbortController en contextdeadlines voorkomt dat 'zombie requests' ongemerkt resources blijven consumeren terwijl de breaker al geactiveerd is.

State transitions en drempelwaarden kalibreren

Het bepalen van de juiste drempelwaarden is een delicaat evenwicht tussen bescherming en beschikbaarheid. Te agressieve instellingen veroorzaken onnodige provider-switches (flapping), terwijl te conservatieve instellingen de eigen backend laten vollopen.

De volgende parameters vormen de standaardconfiguratie voor een robuuste LLM-circuit breaker in productie:

Fallback-strategieën bij een geopend circuit

Zodra de circuit breaker de status Open bereikt, moet de applicatie een gedefinieerde uitwijkroute volgen. Een circuit breaker die simpelweg een foutmelding naar de eindgebruiker stuurt, lost slechts de helft van het probleem op. De kracht zit in het naadloos overschakelen naar alternatieve verwerkingsroutes.

Wanneer de primaire LLM-provider uitvalt, zijn er drie bewezen patronen om de continuïteit te handhaven:

  1. Provider Failover (Multi-Provider Routing): Het verkeer wordt direct gerouteerd naar een secundaire provider met een vergelijkbare modelfamilie (bijvoorbeeld van een primair inference-cluster naar een alternatieve aanbieder).
  2. Model Downgrading: Het verzoek schakelt over naar een kleiner, sneller en lokaal of betrouwbaarder model met lagere latency en ruimere capaciteit.
  3. Graceful Degradation: Complexe agentic taken worden teruggeschaald naar deterministische zoekopdrachten, statische antwoorden of asynchrone wachtrijen.

Voor een gedetailleerde uitwerking van deze architecturen en hoe je applicatielogica intact houdt tijdens gedeeltelijke storingen, lees je de gids over graceful degradation bij LLM-uitval. Zorg er bij geautomatiseerde failover altijd voor dat idempotentiestempels behouden blijven, zoals beschreven in de handleiding over idempotentie bij LLM-calls, zodat eindgebruikers bij een plotselinge providerwissel nooit dubbel worden gefactureerd voor een deels voltooide generatie.

In complexe autonome systemen kunnen open circuits bovendien kettingreacties veroorzaken in de beslisboom van een agent. Om te begrijpen hoe dit zich manifesteert in cyclische aanroepen, bekijk je de analyse over agentic loops debuggen op ons community-platform.

Implementatievoorbeeld: Asynchrone Python Circuit Breaker

Onderstaande productie-georiënteerde implementatie toont een asynchrone circuit breaker in Python. Deze breaker bewaakt de TTFT en statuscodes, gebruikt een tijdvenster met lock-synchronisatie en ondersteunt veilige probing in de Half-Open status.

import asyncio
import time
from enum import Enum
from typing import Callable, Any, Dict, Optional

class CircuitState(Enum):
  CLOSED = "CLOSED"
  OPEN = "OPEN"
  HALF_OPEN = "HALF_OPEN"

class LLMCircuitBreaker:
  def __init__(
    self,
    name: str,
    failure_threshold: float = 0.5,
    recovery_timeout: float = 30.0,
    min_samples: int = 5,
    sample_window: float = 60.0,
    slow_call_duration: float = 12.0
  ):
    self.name = name
    self.failure_threshold = failure_threshold
    self.recovery_timeout = recovery_timeout
    self.min_samples = min_samples
    self.sample_window = sample_window
    self.slow_call_duration = slow_call_duration

    self.state = CircuitState.CLOSED
    self.history = []  # tuples van (timestamp, is_failure, is_slow)
    self.last_state_change = time.time()
    self.lock = asyncio.Lock()
    self.half_open_in_flight = 0

  def _clean_history(self, now: float):
    cutoff = now - self.sample_window
    self.history = [h for h in self.history if h[0] > cutoff]

  async def can_execute(self) -> bool:
    async with self.lock:
      now = time.time()
      if self.state == CircuitState.OPEN:
        if now - self.last_state_change >= self.recovery_timeout:
          self.state = CircuitState.HALF_OPEN
          self.last_state_change = now
          self.half_open_in_flight = 1
          return True
        return False

      if self.state == CircuitState.HALF_OPEN:
        if self.half_open_in_flight < 2:
          self.half_open_in_flight += 1
          return True
        return False

      return True

  async def record_result(self, success: bool, duration: float):
    async with self.lock:
      now = time.time()
      is_slow = duration > self.slow_call_duration
      is_failure = (not success) or is_slow

      if self.state == CircuitState.HALF_OPEN:
        if success and not is_slow:
          self.state = CircuitState.CLOSED
          self.history.clear()
        else:
          self.state = CircuitState.OPEN
        self.last_state_change = now
        self.half_open_in_flight = 0
        return

      if self.state == CircuitState.CLOSED:
        self.history.append((now, is_failure, is_slow))
        self._clean_history(now)

        if len(self.history) >= self.min_samples:
          failures = sum(1 for h in self.history if h[1])
          rate = failures / len(self.history)
          if rate >= self.failure_threshold:
            self.state = CircuitState.OPEN
            self.last_state_change = now

  async def execute(self, func: Callable, fallback: Optional[Callable] = None, *args, **kwargs) -> Any:
    if not await self.can_execute():
      if fallback:
        return await fallback(*args, **kwargs)
      raise RuntimeError(f"Circuit '{self.name}' is OPEN: LLM upstream niet beschikbaar.")

    start = time.time()
    try:
      result = await func(*args, **kwargs)
      await self.record_result(success=True, duration=time.time() - start)
      return result
    except Exception as exc:
      await self.record_result(success=False, duration=time.time() - start)
      if fallback:
        return await fallback(*args, **kwargs)
      raise exc

In dit voorbeeld bewaakt de circuit breaker niet alleen of de call crasht met een exceptie, maar ook of de uitvoeringstijd de slow_call_duration overschrijdt. Een LLM-provider die 20 seconden doet over een simpele prompt wordt hiermee effectief geïsoleerd voordat alle beschikbare applicatiesockets geblokkeerd raken.

Valkuilen in productie: Thundering herds en gedistribueerde state

Het draaien van circuit breakers op productieschaal introduceert specifieke concurrency- en architectuurvraagstukken. De twee meest voorkomende operationele risico's zijn:

1. De Thundering Herd bij State Reset

Wanneer een circuit van Open naar Half-Open overgaat en vervolgens herstelt naar Closed, zien duizenden wachtende clients tegelijkertijd dat de provider weer 'gezond' is. Als deze clients allemaal tegelijk hun uitgestelde requests afvuren, crasht de zojuist herstelde provider onmiddellijk opnieuw. Dit cyclische fenomeen (flapping) moet worden voorkomen door:

2. In-Memory versus Gedistribueerde Circuit State (Redis)

Draait je applicatie over tientallen Kubernetes pods of serverless containers, dan moet de status van het circuit worden gecoördineerd. Een lokaal in-memory circuit per container werkt prima bij hoge volumes per node, maar bij sterk gefragmenteerde microservices weet node A niet dat node B continu 503-fouten ontvangt.

Voor gecentraliseerde controle kan de status worden gesynchroniseerd via Redis met behulp van snelle Lua-scripts of Redis-hashes. Houd hierbij echter rekening met de netwerklatency naar Redis toe: de check mag niet meer dan 1-2 milliseconden kosten om de totale verwerkingstijd niet onnodig te belasten.

Monitoring, alerting en integratie met runbooks

Een transformerende circuit breaker moet realtime zichtbaar zijn in je observability-stack. Wanneer een circuit van toestand verandert, is dat per definitie een operationele gebeurtenis die alerting rechtvaardigt.

Zorg voor de volgende meetpunten in Prometheus, Datadog of OpenTelemetry:

Koppel alerts op status Open direct aan je storingsprocedures. Wanneer het circuit langer dan 5 minuten open blijft, dient het monitoringplatform automatisch te escaleren naar de dienstdoende engineers. Dit vormt de directe input voor gestandaardiseerde incidentbestrijding, zoals gedocumenteerd in het runbook voor een LLM-storing.

Conclusie

Circuit breakers zijn geen luxe maar een absolute randvoorwaarde voor betrouwbare LLM-integraties in productie. Door expliciet te sturen op latency-drempels, streaming-onderbrekingen en strikte probe-volumes in de Half-Open fase, voorkom je dat een instabiele AI-provider je complete backend platlegt. Gecombineerd met geautomatiseerde fallbacks en nauwkeurige telemetrie bouw je een veerkrachtige architectuur die operationele continuïteit garandeert, ongeacht de grillen van externe cloudmodellen.