Gepubliceerd op api.llmnet.nl | Ontwikkelaarsgids

Timeouts, annulering en deadline-budgetten bij LLM-calls

In de wereld van traditionele API-ontwikkeling duren de meeste netwerkverzoeken enkele tientallen tot honderden milliseconden. Zodra we echter met Large Language Models (LLM's) gaan werken, veranderen de regels van het spel drastisch. Een simpele LLM-call kan zomaar vijf seconden duren, en een complexe generatietaak kan oplopen tot meer dan dertig seconden. Dit fundamentele verschil in responstijden introduceert aanzienlijke risico's voor de stabiliteit van je applicatie.

Wanneer je geen rekening houdt met deze vertragingen, kunnen trage LLM-calls je hele architectuur blokkeren, connectiepools uitputten en leiden tot torenhoge kosten. In dit artikel duiken we diep in de strategieën om met deze onvoorspelbare responstijden om te gaan. We bespreken waarom trage calls zo gevaarlijk zijn, hoe je verstandige timeouts instelt per soort taak, het belang van het correct afbreken van streams, en hoe je een zogenaamd 'deadline-budget' hanteert over meerdere calls heen.

Waarom trage LLM-calls je hele keten blokkeren

Wanneer een webapplicatie of backend-service een inkomend verzoek ontvangt, wijst de server daar doorgaans een "worker", thread of connectie aan toe. Deze resource blijft gereserveerd totdat het verzoek volledig is afgehandeld. Dit model werkt uitstekend wanneer API-calls snel zijn, maar het faalt spectaculair in de context van LLM's als je niet oppast.

Stel dat je server een limiet heeft van 100 gelijktijdige connecties. Als elke LLM-call 10 seconden duurt, kan je server maximaal 10 verzoeken per seconde (100 / 10) verwerken. Komt het verkeer daarboven, dan lopen de wachtrijen vol. Nieuwe gebruikers krijgen geen antwoord meer, en de server weigert uiteindelijk dienst. Dit fenomeen staat bekend als resource exhaustion of thread starvation.

Daarnaast heb je te maken met het rimpeleffect (cascade-effect) in microservice-architecturen. Als microservice A afhankelijk is van microservice B, en B wacht traag op een LLM-provider (zoals OpenAI, Anthropic of een lokaal open-source model), dan blijft service A ook hangen. Zonder strak geconfigureerde timeouts slaat de vertraging over naar je hele infrastructuur. Dit vereist het bouwen van robuuste integraties waarbij falingen op tijd worden opgevangen en geïsoleerd.

Verstandige timeouts per soort taak

Een veelgemaakte fout is het instellen van een enkele, globale timeout voor alle LLM-interacties. Een generiek getal (bijvoorbeeld 30 seconden) is zelden passend. De aard van de prompt bepaalt de verwachte rekentijd. We maken onderscheid tussen twee soorten tijdsmetingen bij LLM's: Time-To-First-Token (TTFT) en Total Generation Time.

De TTFT is de tijd die het model nodig heeft om de prompt te verwerken en het allereerste woord terug te sturen. Zodra het eerste woord is gegenereerd, is de resterende tijd direct afhankelijk van het aantal tokens in de output. Een korte classificatietaak is snel klaar, terwijl het schrijven van een uitgebreid rapport veel langer duurt. Stem je timeouts daarom af op de specifieke use-case.

Soort LLM-Taak Verwachte Output Aanbevolen Timeout (Totaal) Toelichting
Intent-herkenning / Routing 1 tot 10 tokens (JSON/Enum) 2 - 4 seconden Dit moet razendsnel zijn. Als de LLM niet binnen 4 seconden kan bepalen wat de intentie is, breek je af en gebruik je een fallback (bijv. keyword matching).
Standaard Chat (Klantenservice) 100 tot 300 tokens 15 - 20 seconden Acceptabele wachttijd voor een eindgebruiker in een chat-interface, mits streaming wordt gebruikt om de perceptie van snelheid te verhogen.
Complexe RAG Synthese 500+ tokens (incl. bronnen) 30 - 45 seconden Bij Retrieval-Augmented Generation moet het model grote hoeveelheden context verwerken. Een langere timeout is hier onvermijdelijk.
Autonome Agents / Multi-step Variabel, meerdere calls 60+ seconden Hierbij voert de agent zelfstandig tools uit. Deze taken verlopen vaak asynchroon op de achtergrond; de web-request timeout mag deze taak niet zomaar doden.

Streaming afbreken: hoe en waarom

Omdat het genereren van tekst door een LLM zo lang kan duren, gebruiken vrijwel alle moderne AI-applicaties streaming (vaak via Server-Sent Events, of SSE). Hiermee zie je de tekst direct op het scherm verschijnen. Je kunt meer lezen over het correct implementeren hiervan in ons artikel over streaming responses.

Echter, wat gebeurt er als de gebruiker zijn browser sluit, of op een 'Stop Genereren' knop klikt terwijl de stream halverwege is? In een naïeve implementatie blijft jouw backend-server verbonden met de LLM-provider, genereert alle resterende tokens en gooit ze vervolgens weg. Dit kost niet alleen onnodig netwerkverkeer, maar je betaalt ook nog eens voor alle gegenereerde tokens per API-call (bij veel modellen betaal je immers per duizend tokens).

Het is daarom essentieel om een cancellation signal te propageren. Wanneer de TCP-connectie met de cliënt verbreekt (bijvoorbeeld doordat de gebruiker wegnavigeert), moet jouw backend dit detecteren. Zodra dit gebeurt, dien je de onderliggende HTTP-call naar de LLM-API direct te sluiten of te annuleren. Vrijwel alle officiële SDK's van modelaanbieders ondersteunen tegenwoordig zogenaamde abort controllers (in JavaScript) of context signals (in Go/Python) om een lopende stream netjes af te breken aan de API-kant.

Opgeschoonde retries na een timeout

Wanneer een LLM-call faalt met een timeout, is de instinctieve reactie van veel ontwikkelaars om het verzoek automatisch opnieuw in te dienen (een "retry"). Hoewel retries nuttig zijn bij tijdelijke netwerkstoringen (zoals een 502 Bad Gateway), zijn ze levensgevaarlijk bij timeouts zonder een goed doordacht plan.

Stel je voor dat een LLM-provider tijdelijk overbelast is. Verzoeken duren langer dan de ingestelde 15 seconden en eindigen in een timeout. Als jouw applicatie onmiddellijk (en herhaaldelijk) hetzelfde verzoek opnieuw probeert, verdubbel of verdrievoudig je de belasting op de al worstelende API. Dit leidt tot een zogenaamde Thundering Herd of Retry Storm, waarbij de situatie alleen maar verslechtert.

Retries na een timeout moeten daarom strikt gecontroleerd worden. Gebruik altijd een mechanisme met exponentiële wachttijden en "jitter" (willekeurigheid). Bekijk ons uitgebreide artikel over retries en backoff-strategieën voor de exacte implementatiedetails. Daarnaast is het bij timeouts cruciaal om te onthouden dat je niet zeker weet of de API jouw verzoek deels heeft verwerkt. Voor pure (lees-)generatietaken is dit geen probleem, maar pas op als je LLM ook acties uitvoert (zoals het aanroepen van een database in een agentic workflow).

Het concept van Deadline-Budgetten (Total Request Budget)

Nu we de basisprincipes van timeouts hebben besproken, komen we bij het meest geavanceerde, maar tevens meest cruciale concept in AI-architectuur: het Deadline-Budget.

In veel moderne AI-systemen, vooral bij basis RAG systemen (Retrieval-Augmented Generation), bestaat een enkel gebruikersverzoek uit meerdere opeenvolgende stappen. Een typisch stroomschema ziet er als volgt uit:

  1. De gebruiker stelt een vraag via de frontend (HTTP Request).
  2. Jouw API Gateway staat maximaal 10 seconden toe voor het hele proces.
  3. Je roept een embedding-model aan om de vraag te vectoriseren (kost 1.5 seconde).
  4. Je zoekt in de vector-database naar relevante context (kost 0.5 seconde).
  5. Je stuurt de context en de vraag naar de hoofd-LLM.

Als je bij stap 5 een hardcoded timeout van 10 seconden op de LLM-call zet, loop je tegen een groot probleem aan. De eerste twee stappen hebben al 2 seconden verbruikt. Als de LLM er 9 seconden over doet, blijft de call succesvol binnen de LLM-timeout van 10 seconden. Echter, de totale verwerkingstijd is nu 11 seconden (2 + 9). Dit overschrijdt de 10-seconden limiet van je API Gateway. De gateway zal het verzoek aan de gebruiker afbreken met een "504 Gateway Timeout", terwijl jouw backend op de achtergrond nutteloos geld staat te verbranden aan de LLM-API.

Om dit op te lossen, gebruik je een Deadline-Budget. Je berekent bij aanvang van het verzoek wanneer de absolute uiterste deadline is, en je berekent voor elke volgende actie hoeveel "budget" (tijd) er nog over is.

Concreet Patroon: Context en Cancellation in de Praktijk

Om een deadline-budget succesvol te implementeren, moet je de nog resterende tijd dynamisch doorgeven aan je LLM-client. Hier is een conceptueel voorbeeld van hoe je dit in Python implementeert. Het patroon zorgt ervoor dat we nooit langer wachten op de LLM dan we in totaal als budget beschikbaar hebben.

import time
import asyncio
from typing import Optional

class DeadlineBudget:
    def __init__(self, total_budget_seconds: float):
        self.start_time = time.time()
        self.total_budget = total_budget_seconds

    def remaining(self) -> float:
        elapsed = time.time() - self.start_time
        left = self.total_budget - elapsed
        return max(0.0, left)

    def is_expired(self) -> bool:
        return self.remaining() <= 0

async def handle_rag_request(user_query: str):
    # De API Gateway accepteert maximaal 10 seconden voor de hele keten
    budget = DeadlineBudget(total_budget_seconds=10.0)

    # Stap 1: Embeddings (neemt wat tijd in beslag)
    if budget.is_expired():
        return "Timeout vóór embedding"
    vector = await get_embeddings(user_query, timeout=budget.remaining())

    # Stap 2: Database zoekopdracht
    if budget.is_expired():
        return "Timeout vóór database zoekopdracht"
    context = await fetch_from_vector_db(vector, timeout=budget.remaining())

    # Stap 3: LLM Call met het RESTANT van het budget
    time_left_for_llm = budget.remaining()
    if time_left_for_llm <= 0.5:
        # Als we minder dan een halve seconde over hebben, is het nutteloos om 
        # nog een zware LLM call te starten. Breek direct af.
        return "Niet genoeg tijd over om een antwoord te genereren."

    try:
        # We geven exact de resterende tijd mee als timeout voor deze call
        response = await call_llm_api(
            prompt=user_query, 
            context=context, 
            timeout=time_left_for_llm
        )
        return response
    except asyncio.TimeoutError:
        return "De LLM was te traag en overschreed het deadline-budget."
Belangrijk inzicht: Merk op in de bovenstaande code dat we bij stap 3 controleren of we nog wel een realistische hoeveelheid tijd over hebben (0.5 seconden). Zelfs de snelste modellen hebben een TTFT van honderden milliseconden. Als er bijna geen budget meer over is, is de kans op succes nihil. Het is goedkoper (en sneller voor de gebruiker) om dan direct te falen.

Conclusie & Best Practices

Het werken met LLM API's vraagt om een fundamenteel andere benadering van netwerk-timeouts dan je wellicht gewend bent. Samenvattend zijn dit de belangrijkste stelregels voor een gezonde, kostenefficiënte applicatie-architectuur:

Door deze strategieën toe te passen, voorkom je dat haperende AI-modellen een domino-effect veroorzaken in jouw backend, houd je je API-kosten onder controle, en verbeter je de betrouwbaarheid en gebruikerservaring aanzienlijk.