Circuit breakers implementeren voor instabiele LLM-APIs
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:
- Extreme latency-variatie: Een geslaagde generatie kan afhankelijk van het promptformaat en de output-lengte variëren tussen 800 milliseconden en 45 seconden. Een generieke time-out triggert te snel op lange prompts of te traag op echte netwerkophoping.
- Streaming en time-to-first-token (TTFT): Fouten treden vaak niet op tijdens de initiële TCP-handshake, maar halverwege een Server-Sent Events (SSE) stream. Het verbreken van de verbinding na het genereren van 200 tokens vereist een andere foutclassificatie dan een directe weigering.
- Asynchrone rate limits (HTTP 429): Providers hanteren aparte limieten voor Requests Per Minute (RPM) en Tokens Per Minute (TPM). Een 429-respons betekent niet noodzakelijk dat de provider offline is, maar vereist een specifieke terugvalprocedure in plaats van een harde circuitblokkade.
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.
• 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:
- Minimaal sample-volume (Minimum Request Volume): Stel dit in op minimaal 10 verzoeken binnen het venster. Dit voorkomt dat het circuit direct opent als 2 van de 2 nachtelijke verzoeken toevallig time-outen.
- Faaldrempel (Failure Rate Threshold): Een percentage van 40% tot 50% mislukte aanroepen binnen het tijdvenster is gebruikelijk om incidentele netwerkfoutjes te tolereren, maar structurele uitval tijdig af te kappen.
- Trage oproepdrempel (Slow Call Rate Threshold): Een verzoek dat langer duurt dan 3 keer de gemiddelde p95-latency van de provider (bijvoorbeeld > 15 seconden voor een standaard completion) moet worden gemarkeerd als 'slow call'. Als 60% van de calls 'slow' is, opent het circuit, zelfs als de calls uiteindelijk technisch slagen.
- Afkoelperiode (Wait Duration in Open State): De wachttijd voordat de Half-Open status wordt geactiveerd. Voor LLM-providers is 30 tot 60 seconden ideaal. Kortere tijden geven overbelaste clusters onvoldoende hersteltijd.
- Permitted Probes in Half-Open: Beperk het aantal testverzoeken in Half-Open toestand tot exact 2 tot 5 sequentiële aanroepen.
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:
- 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).
- Model Downgrading: Het verzoek schakelt over naar een kleiner, sneller en lokaal of betrouwbaarder model met lagere latency en ruimere capaciteit.
- 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:
- Het toevoegen van jitter aan de cool-down timer, zodat verschillende worker nodes niet op exact dezelfde seconde hun Half-Open probe uitvoeren.
- Het hanteren van een rate limiter (token bucket) die de doorvoer na herstel geleidelijk opvoert in plaats van direct 100% capaciteit toe te laten.
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:
llm_circuit_breaker_state{name="openai-gpt4o"}: Gauge metric (0 = Closed, 1 = Half-Open, 2 = Open).llm_circuit_breaker_rejections_total: Counter metric voor het aantal lokaal afgewezen of omgeleide verzoeken.llm_circuit_breaker_transitions_total: Counter metric voor het aantal toestandsveranderingen (om flapping te detecteren).
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.


