In gedistribueerde systemen is netwerkfoutafhandeling een constante uitdaging. Wanneer je applicatie communiceert met externe Large Language Model (LLM) API's, versterken de trage responstijden en hoge rekenkosten deze problematiek. Het willekeurig opnieuw proberen van een mislukte HTTP-aanroep kan leiden tot dubbele facturatie, inconsistente systeemtoestanden en ongewenste acties in externe systemen.
Idempotentie is het ontwerpprincipe dat garandeert dat het meerdere keren uitvoeren van dezelfde operatie exact hetzelfde resultaat oplevert als een enkele uitvoering. Dit artikel behandelt waarom LLM-calls van nature niet idempotent zijn, hoe je een robuust idempotentiepatroon bouwt, en hoe je omgaat met neveneffecten en gedeeltelijk mislukte streaming-antwoorden.
Waarom dubbele verwerking een reëel probleem is
Dubbele verwerking bij API-interacties ontstaat zelden doordat een ontwikkelaar per ongeluk dezelfde functie twee keer aanroept. Het is vrijwel altijd het resultaat van de onderliggende infrastructuur en menselijk gedrag:
- Timeouts bij geslaagde calls: De LLM-provider verwerkt de prompt succesvol, maar het netwerk tussen de provider en jouw server valt weg voordat de HTTP 200 OK de client bereikt. Jouw applicatie ziet een HTTP-timeout en weet niet of het verzoek is verwerkt. Voor meer details over het correct instellen van netwerkgrenzen, zie ons artikel over timeouts en cancellation.
- Automatische retries na netwerkfouten: Client-libraries of API gateways proberen bij een 503 Service Unavailable of een TCP-reset vaak automatisch de call opnieuw uit te voeren. Als het eerste verzoek op de achtergrond toch is geaccepteerd, verwerkt de provider de prompt twee keer. Meer hierover vind je in de gids over retries en backoff.
- Herstart van achtergrondworkers en at-least-once queues: Berichtenwachtrijen zoals RabbitMQ, AWS SQS of Apache Kafka garanderen doorgaans 'at-least-once' bezorging. Als een worker crasht nét voordat hij het verwerkte bericht bevestigt (ACK), pakt een andere worker hetzelfde werk opnieuw op. Bij het opzetten van webhooks en asynchrone taken is dit een fundamenteel aandachtspunt.
- Gebruikersgedrag: Een eindgebruiker klikt dubbel op de knop "Genereer rapport" omdat de interface niet direct visuele feedback geeft.
Waarom een LLM-call van nature NIET idempotent is
In tegenstelling tot een HTTP GET-verzoek of een SQL UPDATE die een vaste waarde instelt, is een standaard HTTP POST naar een /v1/chat/completions endpoint inherent niet-idempotent op drie vlakken:
1. Directe financiële schade
Elke verwerkte prompt kost tokens. Wanneer jouw systeem een prompt van 4.000 tokens door een netwerkstoring drie keer verstuurt, betaal je drie keer voor dezelfde invoer. Op schaal leidt dit tot een aanzienlijke lek in je API-budget.
2. Niet-deterministische output
Tenzij de temperature parameter strikt op 0.0 staat (en zelfs dan garanderen niet alle providers determinisme vanwege parallelle GPU-berekeningen), levert elke uitvoering van dezelfde prompt een andere tekst op. Als twee parallelle workers dezelfde taak uitvoeren, genereren ze twee afwijkende antwoorden. Welke van de twee sla je op in de database? Dit veroorzaakt race conditions en datainconsistentie.
3. Neveneffecten (Function Calling en Agentic Workflows)
Wanneer een LLM wordt ingezet als agent met 'function calling' mogelijkheden, kan een dubbele call verregaande gevolgen hebben. Een model dat de opdracht krijgt om "de klant een bevestigingsmail te sturen en een ticket aan te maken", zal bij een dubbele verwerking twee tickets aanmaken en twee e-mails versturen.
Kernregel: Beschouw elke LLM-API-call als een operatie met neveneffecten. Vertrouw er nooit op dat de provider de call automatisch voor je ontdubbelt, tenzij de provider expliciet een idempotentie-header ondersteunt.
Het patroon van de Idempotentiesleutel (Idempotency Key)
Om een niet-idempotente LLM-call idempotent te maken, introduceren we een unieke Idempotency Key. Dit patroon werkt volgens het principe dat de vragende partij een uniek kenmerk meestuurt met het verzoek. De ontvangende of tussenliggende laag controleert dit kenmerk voordat de LLM wordt aangeroepen.
Sommige LLM-providers ondersteunen een custom HTTP-header voor idempotentie (zoals Idempotency-Key: <key>). De exacte headernaam en ondersteuning verschillen per provider. Indien de provider dit niet ondersteunt, moet je deze laag in je eigen applicatie-architectuur of API-gateway implementeren.
Hoe kies of leid je de sleutel af?
De sleutel moet de unieke intentie van de operatie vertegenwoordigen:
- Expliciete UUID: Genereer een UUIDv4 aan de bron (bijvoorbeeld in het frontend bij de gebruikersactie of bij het aanmaken van de taak in de queue). Dit is de meest robuuste methode.
- Deterministic Hash: Leid de sleutel af door een cryptografische hash (SHA-256) te maken van de stabiele invoerparameters:
hash(user_id + task_type + input_payload). Let op dat je geen dynamische parameters zoals `timestamp` meeneemt in de hash, anders vervalt het effect van de ontdubbeling.
Bewaartermijn en opslag
Sla de status van idempotentiesleutels op in een snelle, centrale key-value store zoals Redis. De levensduur (TTL) van de sleutel hangt af van de use case, maar 24 tot 48 uur is voor de meeste LLM-toepassingen voldoende om netwerk-retries en queue-herhalingen op te vangen.
Implementatie: Ontdubbeling in je eigen applicatielaag
Onderstaande pseudocode illustreert hoe je met behulp van Redis een idempotentiestructuur bouwt rondom een LLM-API-call. Dit voorkomt dat twee gelijktijdige verzoeken dezelfde LLM-call uitvoeren en zorgt ervoor dat een latere retry het reeds gegenereerde antwoord terugkrijgt.
import redis
import time
import json
db = redis.Redis(host='localhost', port=6379, db=0)
def execute_idempotent_llm_call(idempotency_key, prompt_data):
lock_key = f"lock:{idempotency_key}"
response_key = f"response:{idempotency_key}"
# 1. Controleer of het resultaat al bestaat
cached_response = db.get(response_key)
if cached_response:
return json.loads(cached_response), "CACHE_HIT"
# 2. Probeer een distributed lock te verkrijgen (atomic SETNX)
# TTL van 60 seconden voorkomt dat een gecrashte worker de key voor altijd blokkeert
acquired = db.set(lock_key, "PROCESSING", nx=True, ex=60)
if not acquired:
# Een andere worker verwerkt dit verzoek momenteel. Wacht en poll.
return wait_for_concurrent_request(response_key)
try:
# 3. Voer de werkelijke LLM API call uit
llm_result = call_external_llm_api(prompt_data)
# 4. Sla het resultaat op met een TTL van 24 uur (86400 sec)
db.set(response_key, json.dumps(llm_result), ex=86400)
return llm_result, "EXECUTED"
finally:
# Verwijder de lock zodra het werk gedaan (of mislukt) is
db.delete(lock_key)
def wait_for_concurrent_request(response_key, timeout=30):
start = time.time()
while time.time() - start < timeout:
cached_response = db.get(response_key)
if cached_response:
return json.loads(cached_response), "CACHE_HIT_WAIT"
time.sleep(0.5)
raise TimeoutError("LLM verwerking duurde te lang bij concurrent verzoek.")
Merk op dat dit patroon verschilt van algemene antwoord-caching. Terwijl caching bedoeld is om prestaties te verbeteren en kosten te drukken voor identieke vragen, is dit patroon specifiek ontworpen voor correctheid en het voorkomen van dubbele verwerking bij foutsituaties. Raadpleeg ons artikel over caching van LLM-antwoorden voor de verschillen in implementatie.
API-call ontdubbelen vs. Neveneffecten (Side Effects) ontdubbelen
Het beveiligen van de HTTP-call naar de LLM-provider is pas de helft van de oplossing. In moderne softwarearchitecturen verwerkt een LLM niet alleen tekst, maar genereert het gestructureerde data waarmee vervolgacties worden aangestuurd.
Er is een essentieel verschil tussen twee niveaus van ontdubbeling:
- Transport-niveau (API-call): Het voorkomen dat de HTTP-aanroep naar de LLM-provider twee keer wordt uitgevoerd.
- Domein-niveau (Neveneffecten): Het voorkomen dat de actie die voortvloeit uit de LLM-output (bijvoorbeeld het bijwerken van een database of het versturen van een pushnotificatie) twee keer wordt uitgevoerd.
Stel dat de LLM-call succesvol is en het antwoord opslaat in de cache, maar de worker crasht nét voordat de vervolginformatie naar de CRM-database geschreven wordt. Bij de volgende retry van de queue leest de worker het antwoord direct uit de cache (transport-idempotentie geslaagd), maar voert de CRM-update alsnog voor de eerste keer uit.
Om domein-idempotentie te bereiken, moet je database-operaties uitvoeren met behulp van upserts (INSERT ... ON CONFLICT DO UPDATE) en externe systemen aanroepen met unieke transactie-id's die zijn afgeleid van de oorspronkelijke idempotency_key.
Omgaan met gedeeltelijke fouten en streaming-antwoorden
Streaming-responses (via Server-Sent Events of WebSockets) vormen een bijzondere uitdaging voor idempotentie. Omdat de data in brokstukken (chunks) binnenkomt, kan een verbinding halverwege verbreken nadat er al 500 van de 1000 tokens zijn ontvangen.
Als een streaming call halverwege afbreekt en je voert een eenvoudige retry uit, leidt dit tot problemen:
- Als je de al ontvangen tekst negeert en opnieuw begint, heb je dubbele kosten gemaakt voor de eerste 500 tokens.
- Als je het eerste deel al hebt opgeslagen in je database, zorgt een retry voor dubbele of verminkte content.
Strategieën voor streaming-idempotentie
Voor streaming-scenario's zijn er twee gangbare benaderingen:
1. Bufferen tot voltooiing: Sla streaming-chunks op in een tijdelijke geheugenbuffer (of Redis). Pas wanneer de stream het expliciete afsluitteken (zoals [DONE] of finish_reason: "stop") bereikt, markeer je de idempotency_key als voltooid en schrijf je het resultaat naar de permanente opslag. Mislukt de stream halverwege? Dan gooi je de buffer leeg en voert de retry een schone nieuwe call uit.
2. Append-only met sessie-tracking: Sla elke chunk op in de database gekoppeld aan een uniek chunk_index en de idempotency_key. Bij een hervatting vraagt de client de huidige max(chunk_index) op en stuurt de LLM-prompt opnieuw met een instructie om verder te gaan vanaf dat punt. Dit vereist echter specifieke ondersteuning van het model en de provider.
Samenspel met retries en exponential backoff
Idempotentie en retries zijn twee kanten van dezelfde medaille. Retries zonder idempotentie zijn gevaarlijk; idempotentie zonder retries is nutteloos bij netwerkstoringen.
Wanneer je een retry-mechanisme met exponential backoff en jitter implementeert, moet de idempotency_key constant blijven over alle opeenvolgende pogingen van dezelfde logische actie.
- Pointers bij retries: Zorg dat bij Poging 1, Poging 2 en Poging 3 exact dezelfde
idempotency_keyin de header of payload wordt meegegeven. - Foutcodes bij locks: Als een retry plaatsvindt terwijl de vorige poging nog bezig is in de LLM-pipeline, moet de API niet direct opnieuw proberen, maar de lock-status respecteren (bijvoorbeeld door een HTTP 409 Conflict of 429 Too Many Requests terug te geven aan de caller).
Checklist voor idempotentie bij LLM-integraties
Gebruik deze checklist bij het ontwerpen van je LLM-integratielaag:
- Unieke identificatie: Genereer je aan de bron (frontend/client) een unieke sleutel per gebruikersactie of taak?
- Provider-ondersteuning: Controleer je of de specifieke LLM-provider een
Idempotency-Keyheader ondersteunt en wat de exacte voorwaarden zijn? - Eigen opslaglaag: Is er een snelle key-value store (zoals Redis) aanwezig met gedistribueerde locks (
SETNX) om concurrent verzoeken te blokkeren? - TTL beheer: Hebben de opgeslagen idempotentie-records en locks een gepaste automatische vervaldatum (TTL)?
- Domein-ontdubbeling: Zijn de vervolgacties (database-writes, e-mails, webhooks) eveneens beschermd tegen dubbele uitvoering middels unieke identifiers?
- Streaming afhandeling: Worden afgebroken streaming-responses correct opgeruimd of gebufferd voordat ze als 'afgerond' worden gemarkeerd?
Door idempotentie vanaf de start op te nemen in je API-architectuur, voorkom je niet alleen onvoorziene kosten bij LLM-providers, maar bouw je ook aan een systeem dat bestand is tegen netwerkstoringen en onverwachte achtergrondherstarts.
Wil je dieper ingaan op het testen van de robuustheid van je LLM-infrastructuur of zoek je advies voor jouw specifieke architectuur? Bekijk de mogelijkheden op LLMNet Consultancy of neem deel aan de discussies op onze LLMNet Community.

