Naar de inhoud
NLEN
Illustratie: Doorvoeroptimalisatie via multi-provider queues

Doorvoeroptimalisatie via multi-provider request queues

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

Bij het opschalen van applicaties die intensief gebruikmaken van Large Language Models lopen architecturen vrijwel direct tegen de harde grenzen van individuele API-providers aan. Rate limits op Requests Per Minute (RPM) en Tokens Per Minute (TPM), wisselende netwerklatencies en onverwachte service-degradaties zorgen voor knelpunten die niet simpelweg met synchrone HTTP-retries opgelost kunnen worden. Wanneer duizenden verzoeken tegelijk binnenkomen, leidt een naïeve architectuur onvermijdelijk tot HTTP 429 foutmeldingen, verhoogde tail-latency en onvoorspelbare faalkosten.

De oplossing voor dit schaalprobleem ligt in het implementeren van een multi-provider request queue architectuur. Door inkomende verzoeken los te koppelen van de directe API-executie en centraal te bufferen, ontstaat een beheerste stroom van verkeer die dynamisch over meerdere upstream-providers verdeeld kan worden. Dit artikel behandelt de operationele architectuur van multi-provider request queues, dispatch-algoritmes, capaciteitsmodellering en foutafhandeling voor veeleisende productiesystemen. Voor een fundamenteel overzicht van de onderliggende routeringsarchitectuur en infrastructuurkeuzes verwijzen we naar het artikel over het zelf hosten van een LLM-gateway.

De anatomie van doorvoerbeperkingen bij LLM-providers

Om een effectief wachtrijsysteem te ontwerpen, moeten we eerst begrijpen hoe LLM-providers doorvoer reguleren. In tegenstelling tot traditionele web-API's, waar capaciteit hoofdzakelijk wordt begrensd door het aantal HTTP-aanroepen per seconde, hanteren AI-aanbieders een meerdimensionaal begrenzingsmodel. Providers monitoren gelijktijdig het absolute aantal verzoeken (RPM), de totale hoeveelheid invoer- en uitvoertokens per minuut (TPM) en in sommige gevallen het aantal gelijktijdige openstaande verbindingen (concurrency limits).

Een extra complicatie is dat tokenverbruik vooraf niet exact vaststaat. Hoewel de invoerlengte vooraf berekend kan worden via tokenizers, is de gegenereerde uitvoerlengte stochastisch. Als een gateway een geschat tokenbudget reserveert op basis van de parameter max_tokens, kan dit leiden tot ernstige onderbenutting van de beschikbare capaciteit wanneer de werkelijke respons aanzienlijk korter is. Omgekeerd kan een te optimistische schatting resulteren in een abrupte overschrijding van de providerlimiet halverwege een minuutvenster.

Wanneer een applicatie afhankelijk is van één enkele provider-account, vormt de toegekende rate-limit tier het absolute plafond van de bedrijfsactiviteit. Door meerdere accounts, regio's of functioneel vergelijkbare providers achter een geabstraheerde wachtrij te plaatsen, kan de geaggregeerde doorvoer lineair meegroeien met het aantal geconfigureerde upstream-verbindingen.

Architectuur van een multi-provider wachtrijsysteem

Een robuuste multi-provider queue splitst de verwerking op in drie onafhankelijke lagen: ingestie, scheduling en dispatching. Deze ontkoppeling voorkomt dat trage externe API's de interne applicatieservers blokkeren en zorgt voor deterministische controle over de verkeersstromen.

Component Primaire Taak Typische Technologie Kritieke Meetwaarde
Ingestielaag Payload-validatie, toewijzing taak-ID, snelle acceptatie (HTTP 202) Rust, Go, Node.js worker Ingestielatency (< 10ms)
Buffer & State Store Gedistribueerde opslag van taken, metadata en prioritering Redis Streams, RabbitMQ, Apache Kafka Queue depth, persistentie-overhead
Scheduler Core Capaciteitscontrole per provider, selectie volgende taak Gedistribueerde scheduler service Scheduling delay, lock contention
Dispatch Pool HTTP-executie, streaming-beheer, statusverwerking Async HTTP client pool met circuit breakers Egress throughput, HTTP 429 ratio

Binnen deze opzet pusht de aanroepende applicatie een generieke LLM-opdracht naar de ingestielaag. De taak bevat specificaties zoals promptdata, minimale modeleisen (bijvoorbeeld contextvenster en gewenste reasoning-capaciteit) en een prioriteitsniveau. Voor een diepgaande analyse van taakclassificatie en het voorrang verlenen aan bedrijfskritieke stromen, zie het artikel over prioriteitswachtrijen voor LLM-taken.

Capaciteitsmodellering: Sliding Windows en Token Buckets

Om een provider optimaal te benutten zonder rate limits te overschrijden, moet de scheduler de actuele status van elke provider lokaal bijhouden. Dit gebeurt via rate-limiting algoritmes die synchroon lopen met de meetmethodes van de externe providers. De meeste grote aanbieders hanteren een glijdend tijdvenster (sliding window) van 60 seconden.

Een naïeve implementatie wacht tot een HTTP 429 optreedt alvorens gas terug te nemen. In een high-throughput omgeving is dit onacceptabel, omdat een 429-fout gepaard gaat met exponentiële backoff-straffen en tijdelijke capaciteitsverspilling. De gateway moet proactief sturen op basis van een lokale token-administratie. Hoe dit algoritme op microseconden-niveau pre-allocaties en refills beheert, wordt gedetailleerd uitgelegd in de gids over het token bucket algoritme in een LLM-gateway.

In een multi-provider context houdt de scheduler voor elke provider een aparte capaciteitsvector bij:

interface ProviderCapacity {
  providerId: string;
  rpmLimit: number;
  rpmRemaining: number;
  tpmLimit: number;
  tpmRemaining: number;
  activeConnections: number;
  maxConcurrency: number;
  lastResetTimestamp: number;
}

Voordat een taak uit de wachtrij wordt gehaald en verzonden, verifieert de scheduler of de beoogde provider voldoende geschatte capaciteit heeft voor zowel het verzoek (1 unit) als het conservatief berekende aantal invoertokens plus een veilige marge voor uitvoertokens.

Dispatching-strategieën over heterogene providers

Niet alle LLM-providers zijn identiek. Zelfs wanneer modellen functioneel equivalent zijn (zoals Claude 3.5 Sonnet via Anthropic direct versus via AWS Bedrock of Google Cloud Vertex AI), verschillen de kosten per token, latency-profielen en toegekende limieten aanzienlijk. Er zijn vier primaire dispatching-strategieën:

1. Waterfalldispatching met capaciteitsoverloop

Verzoeken worden primair naar de goedkoopste of snelste provider gestuurd totdat diens capaciteitsdrempel (bijvoorbeeld 85% van de TPM-limiet) is bereikt. Zodra deze drempel nadert, vloeit overtollig verkeer automatisch over naar de secundaire provider. Dit minimaliseert operationele kosten bij lage volumes en schaalt naadloos op bij pieken.

2. Gewogen Round-Robin op basis van limietverhoudingen

Wanneer meerdere accounts of providers gelijktijdig actief moeten blijven om pooling-capaciteit warm te houden, verdeelt de dispatcher taken proportioneel aan de beschikbare limieten. Als Provider A een limiet heeft van 2.000.000 TPM en Provider B van 500.000 TPM, stuurt de dispatcher verzoeken in een verhouding van 4:1 door.

3. Dynamische Latency-Aware Routing

De scheduler monitort continu de Time To First Token (TTFT) en de totale generatieduur per provider via een rolling average van de laatste 50 requests. Verzoeken worden gedispatched naar de provider die op dat moment de laagste mediane latentie vertoont, mits de capaciteitsbuffers dit toelaten. Dit voorkomt dat taken vastlopen op een provider die kampt met interne overbelasting.

4. Model-Tiering en Taakcomplexiteit

Eenvoudige verwerkingstaken (zoals classificaties of extracties) worden gerouteerd naar snelle, goedkopere providers met hoge limieten, terwijl complexe redeneertaken worden gereserveerd voor zwaardere modellen met lagere limieten. De wachtrij categoriseert taken op basis van vereiste capaciteiten en dispatcht uitsluitend naar compatibele pools.

Implementatievoorbeeld: Gedistribueerde Dispatch Worker

Hieronder staat een implementatievoorbeeld van een Node.js dispatch worker die gebruikmaakt van een centrale capaciteitscontrole en idempotente taakverwerking. Dit script demonstreert hoe taken uit een centrale buffer worden gehaald, geëvalueerd tegen beschikbare providers en veilig worden uitgevoerd met timeouts en foutafhandeling.

import { setTimeout as sleep } from 'node:timers/promises';

class MultiProviderDispatcher {
  constructor(queueClient, capacityTracker, providers) {
    this.queue = queueClient;
    this.capacity = capacityTracker;
    this.providers = providers; // Map van geconfigureerde provider clients
    this.isRunning = false;
  }

  async start() {
    this.isRunning = true;
    while (this.isRunning) {
      try {
        await this.dispatchNextTask();
      } catch (err) {
        console.error('Fout in dispatch loop:', err.message);
        await sleep(100); // Korte pauze bij onverwachte fouten
      }
    }
  }

  async dispatchNextTask() {
    // 1. Zoek welke providers momenteel capaciteit hebben
    const eligibleProviders = await this.capacity.getAvailableProviders();
    if (eligibleProviders.length === 0) {
      // Alle providers zitten vol: wacht kort om CPU-spinning te voorkomen
      await sleep(50);
      return;
    }

    // 2. Claim de oudste taak die compatibel is met beschikbare providers
    const task = await this.queue.leaseNextTask(eligibleProviders);
    if (!task) {
      await sleep(25); // Wachtrij is leeg
      return;
    }

    const selectedProvider = eligibleProviders[0];

    // 3. Reserveer geschatte capaciteit vooraf
    const estimatedTokens = task.inputTokens + (task.maxTokens || 500);
    await this.capacity.reserve(selectedProvider.id, estimatedTokens);

    // 4. Voer de LLM-call asynchroon uit met timeout en foutpad
    this.executeCall(task, selectedProvider, estimatedTokens).catch(err => {
      console.error(`Executiefout voor taak ${task.id}:`, err.message);
    });
  }

  async executeCall(task, provider, reservedTokens) {
    const startTime = Date.now();
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), task.timeoutMs || 30000);

    try {
      const response = await this.providers.get(provider.id).chatCompletion({
        model: task.targetModel,
        messages: task.messages,
        temperature: task.temperature,
        signal: controller.signal
      });

      clearTimeout(timeoutId);
      const actualTokens = response.usage.total_tokens;

      // Corrigeer de daadwerkelijk verbruikte capaciteit
      await this.capacity.adjust(provider.id, reservedTokens, actualTokens);

      // Sla het resultaat op en markeer taak als voltooid
      await this.queue.completeTask(task.id, {
        status: 'SUCCESS',
        result: response,
        latencyMs: Date.now() - startTime,
        providerUsed: provider.id
      });

    } catch (err) {
      clearTimeout(timeoutId);
      await this.capacity.release(provider.id, reservedTokens);

      const isRateLimit = err.status === 429;
      const isTimeout = err.name === 'AbortError';

      if (isRateLimit) {
        // Blokkeer provider tijdelijk in de lokale state
        await this.capacity.penalize(provider.id, 60000);
      }

      // Herbeoordeel taak voor retry of failover
      await this.handleFailure(task, provider.id, err, isRateLimit || isTimeout);
    }
  }

  async handleFailure(task, failedProviderId, error, isRetryable) {
    task.retryCount = (task.retryCount || 0) + 1;

    if (isRetryable && task.retryCount <= 3) {
      // Zet taak terug in de wachtrij en sluit de gefaalde provider uit
      await this.queue.requeueTask(task, { excludeProvider: failedProviderId });
    } else {
      await this.queue.completeTask(task.id, {
        status: 'FAILED',
        error: error.message,
        providerUsed: failedProviderId
      });
    }
  }
}

Backpressure en wachtrijbeheer bij extreme pieken

Een wachtrij kan piekbelastingen absorberen, maar niet oneindig doorgroeien als de structurele instroom groter is dan de gecombineerde capaciteit van alle providers. Zonder expliciete backpressure zal het geheugengebruik van de message broker exploderen, lopen wachttijden op tot onbruikbare niveaus en verlopen taken voordat ze ooit worden opgepakt.

Om stabiliteit te garanderen, moet het systeem beschikken over mechanismen om tijdig in te grijpen bij overbelasting. Voor een compleet overzicht van load-shedding strategieën en het afwijzen van verzoeken bij verzadiging, raadpleegt u de handleiding over backpressure-patronen bij overbelaste LLM-gateways.

Praktische maatregelen voor wachtrijhygiëne omvatten:

Kwaliteitsbewaking en output-consistentie over providers

Een belangrijk risico bij multi-provider dispatching is modeldrift en inconsistentie. Zelfs als twee providers beweren exact hetzelfde open-weight model (zoals Llama 3) te hosten, kunnen verschillen in hardware-architectuur, kwantisatie-niveaus (bijvoorbeeld FP8 versus INT4) en runtime-engines (vLLM, TensorRT-LLM) leiden tot subtiele variaties in de gegenereerde output.

Wanneer closed-source modellen van verschillende families worden gebruikt als fallback-targets (zoals GPT-4o als back-up voor Claude 3.5 Sonnet), wordt dit risico nog groter. Prompts moeten dan strikt geëvalueerd worden op provider-agnostische compatibiliteit, met name bij het gebruik van gestructureerde outputs en tool calls. Om te verifiëren of de taakuitvoering betrouwbaar blijft ongeacht de gekozen upstream provider, is het essentieel om continue evaluaties uit te voeren zoals beschreven in de gids over hoe je een AI-agent evalueert.

Observability en metrieken voor multi-provider queues

Het beheren van een gedistribueerd wachtrij- en dispatchingsysteem vereist nauwkeurige telemetrie. Een dashboard moet in één oogopslag inzicht geven in de wisselwerking tussen wachtrijdynamiek en providerprestaties. De belangrijkste metrics zijn:

Metriek Beschrijving Doelwaarde / Alarmeringsdrempel
Queue Dwell Time (p95) Tijd die een taak doorbrengt in de wachtrij vóór dispatch Interactief: < 500ms; Batch: < 30s
Provider Capacity Utilization Percentage benutte RPM en TPM per provider Streefwaarde: 80-90%; Alarm: > 95%
Provider Error Rate Percentage 429, 500, 503 en network timeouts per provider Alarm bij > 1% over 5-minuten venster
Re-queue Frequency Aantal keren dat taken opnieuw moeten worden aangeboden Moet nagenoeg 0 zijn; piek duidt op onjuiste capaciteitsschatting

Door correlatie tussen wachtrijtijden en provider-statussen kunnen knelpunten snel worden gelokaliseerd. Als de Queue Dwell Time stijgt terwijl de capaciteitsbenutting laag is, duidt dit op een bottleneck in de scheduler of worker-threads. Als de Dwell Time stijgt bij 100% benutting over alle providers, is het toevoegen van extra providercapaciteit of upstream-quota de enige structurele oplossing.

Afwegingen, nadelen en faalmodi

Hoewel multi-provider request queues superieure betrouwbaarheid en doorvoer bieden, brengt het patroon duidelijke nadelen met zich mee:

Conclusie

Doorvoeroptimalisatie via multi-provider request queues transformeert onvoorspelbare, door rate-limits geplaagde LLM-integraties in beheersbare, industriële datasystemen. Door proactieve token-administratie, dynamische taakallocatie en strikte backpressure-mechanismen kan een applicatie pieken moeiteloos opvangen en de volledige breedte van het wereldwijde provider-landschap benutten.