Naar de inhoud
NLEN
Illustratie: Hedged requests voor lagere LLM-tail-latency

Hedged requests inzetten om LLM-tail-latency te verlagen

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

In interactieve systemen en gebruikersgerichte applicaties is de gemiddelde reactietijd van een Large Language Model zelden het grootste knelpunt. Waar een gebruiker gemiddeld binnen 800 milliseconden het eerste token ontvangt, zorgen uitschieters in de staartverdeling (de zogenaamde tail latency zoals p95 en p99) voor onvoorspelbare wachttijden van soms tientallen seconden. In microservice-architecturen is het patroon van hedged requests al decennia bekend om variabiliteit in netwerkverkeer en schijfoperaties op te vangen. Binnen LLM-integraties wint dit patroon snel terrein om trage inference-clusters en tijdelijke wachtrijen bij providers te omzeilen.

Dit artikel hoort binnen het fundament voor betrouwbaarheid en foutafhandeling (pijler A1); raadpleeg het ankerartikel over robuuste integraties om te zien hoe hedged requests zich verhouden tot standaard retries en fallbacks. Waar een gewone retry pas start nadat een aanroep expliciet faalt of een harde timeout bereikt, stuurt een hedged request proactief een tweede kopie van het verzoek naar een parallelle instantie of provider wanneer de initiële respons langer op zich laat wachten dan verwacht. Hieronder bekijken we de theorie, implementatiepatronen, annuleringsmechanismen en trade-offs rondom dubbele tokenkosten.

De anatomie van LLM-tail-latency

De reactietijd van een LLM-API-call bestaat uit twee fundamenteel verschillende fasen: de Time to First Token (TTFT) en de Inter-Token Latency (ITL). De TTFT wordt bepaald door netwerklatency, wachtrijtijd op het inference-cluster van de provider, en de zogeheten prefill-fase waarin de gehele prompt parallel door de GPU-cores wordt verwerkt. Zodra het eerste token gegenereerd is, start de autoregressieve decode-fase, waarbij tokens sequentieel gegenereerd worden. Variantie in de p99-regio ontstaat vrijwel altijd tijdens de prefill- of wachtrijperiode door noisy neighbor-effecten, cluster-herverdeling of plotselinge pieken in de batch-planning van de provider.

Voor een diepere wiskundige analyse van responstijden kun je latency meten met percentielen in plaats van gemiddeldes om inzicht te krijgen in hoe scheef LLM-responstijden werkelijk verdeeld zijn. Als een p50 op 600 milliseconden ligt, maar de p99 oploopt naar 14.000 milliseconden, betekent dit dat één op de honderd interacties een onacceptabele gebruikerservaring oplevert. In complexe agentic chains met tien opeenvolgende LLM-aanroepen degradeert de cumulatieve p90 van het totale systeem al snel tot het niveau van de individuele p99.

Hoe hedged requests werken versus standaard retries

Het concept van hedged requests werd breed bekendgemaakt door Google in de publicatie The Tail at Scale (Dean & Barroso, 2013). Het principe is eenvoudig: stuur een verzoek naar instantie A. Als er na een vooraf berekende drempelwaarde (bijvoorbeeld de historische p90 of p95 van de TTFT) nog geen enkel data-pakket of SSE-token is binnengekomen, stuur je exact hetzelfde verzoek naar instantie B zonder instantie A direct af te breken. Wie als eerste reageert met een valide payload of token-stream, levert het uiteindelijke antwoord; de langzamere verbinding wordt direct geannuleerd.

Eigenschap Standaard Retry Hedged Request Race / Speculative Request
Startmoment Na harde timeout of foutcode (429/5xx) Na dynamische drempel (bijv. p90 TTFT) Direct parallel op t = 0
Extra belasting / kosten Laag (alleen bij harde fouten) Beheerst (alleen bij de traagste 5–10%) Zeer hoog (+100% tokens en calls)
Impact op p99-latency Geen effect op trage, geslaagde calls Drastische verlaging (naar ~p90 niveau) Maximale verlaging ten koste van budget
Complexiteit Laag (sequentieel) Gemiddeld (async timers & cancellation) Gemiddeld (parallelle promises)

Selectie van hedging-drempels: statisch vs adaptief

De effectiviteit van hedging staat of valt met het tijdstip waarop de reserve-aanroep wordt afgevuurd. Vuren we te vroeg (bijvoorbeeld op de mediaan p50), dan verdubbelt het tokenverbruik en de factuur voor de helft van al het verkeer. Vuren we te laat (ruim voorbij de p99), dan biedt hedging nauwelijks voordeel ten opzichte van een normale timeout. De optimale drempel balanceert extra API-kosten tegen de winst in reactietijd.

Er worden in productie twee hoofdvormen gebruikt voor het bepalen van de hedging-vertraging:

Bij het ontwerpen van time-outs en deadlines is het essentieel om hedging in te passen in het totale deadline-budget; bekijk de handleiding over timeouts, annulering en deadline-budgetten om te voorkomen dat geneste timers elkaar tegenwerken.

Streaming responses en het Time to First Token (TTFT) signaal

In streaming-scenario's via Server-Sent Events (SSE) biedt de ontvangst van het allereerste chunk het ideale signaal om de hedging-race te beslissen. Zodra instantie A het eerste chunk data: {"choices": [{"delta": {"content": "..."}}]} verstuurt, weten we dat de prefill-fase succesvol is afgerond en het model daadwerkelijk tokens genereert. Op dat exacte moment kan de gateway eventuele geplande hedged timers direct annuleren.

Als de hedged aanroep naar instantie B reeds is verzonden omdat instantie A de TTFT-drempel overschreed, geldt een first-chunk-wins mechanisme. De verbinding die als eerste een valide SSE-chunk teruggeeft, krijgt de streaming-pijplijn naar de client toegewezen. De andere verbinding wordt per direct afgekapt via een HTTP/2 RST_STREAM frame of het verbreken van de TCP-socket om onnodige netwerkdoorvoer en serverbelasting te minimaliseren.

Architectuurpatronen voor hedged routing

Waar sturen we de hedged request naartoe? Het simpelweg herhalen van hetzelfde verzoek naar hetzelfde provider-endpoint lost lokale netwerkproblemen op, maar helpt niet als het gehele datacenter van de provider overbelast is. Er zijn drie architectonische routes mogelijk:

1. Intra-provider (Andere regio of API-sleutel): Het verzoek wordt gestuurd naar dezelfde provider, maar via een andere geografische regio (bijvoorbeeld us-east versus eu-west) of via een aparte organisatie-tier. Dit vangt lokale clusterverzadiging op zonder verschillen in modelgedrag.

2. Cross-provider met equivalente modellen: Een verzoek dat initieel naar Provider A (bijvoorbeeld Claude 3.5 Sonnet via AWS Bedrock) gaat, wordt gehedged naar Provider B (hetzelfde model rechtstreeks via Anthropic API, of een vergelijkbaar alternatief). Dit beschermt tegen provider-brede storingen.

3. Hybride tier-degradation: Het initiële verzoek gaat naar een zwaar redeneermodel. Duurt de reactie te lang, dan start een hedged call naar een lichter, sneller sub-model dat binnen enkele honderden milliseconden een beknopter maar acceptabel antwoord genereert.

Wanneer een centrale component dit verkeer regelt, kan dit direct worden ondergebracht in een gateway-laag; lees meer over de architectuur in de gids over zelf een LLM-gateway hosten met failover en configuratie.

Implementatievoorbeeld in TypeScript

Hieronder staat een robuuste TypeScript-implementatie van een hedged request runner die streaming TTFT ondersteunt, inclusief AbortController en expliciete foutafhandeling.

interface RequestConfig {
  url: string;
  headers: Record<string, string>;
  body: string;
}

async function fetchWithTTFTHedge(
  primary: RequestConfig,
  hedge: RequestConfig,
  hedgeDelayMs: number,
  hardTimeoutMs: number
): Promise<ReadableStream<Uint8Array>> {
  const primaryController = new AbortController();
  const hedgeController = new AbortController();
  const globalTimeoutController = new AbortController();

  const timeoutId = setTimeout(() => {
    globalTimeoutController.abort(new Error("Global deadline exceeded"));
    primaryController.abort();
    hedgeController.abort();
  }, hardTimeoutMs);

  let winnerResolved = false;

  const executeCall = async (
    config: RequestConfig,
    controller: AbortController,
    isHedge: boolean
  ): Promise<ReadableStream<Uint8Array>> => {
    try {
      const response = await fetch(config.url, {
        method: "POST",
        headers: config.headers,
        body: config.body,
        signal: controller.signal,
      });

      if (!response.ok || !response.body) {
        throw new Error(`HTTP fout: ${response.status}`);
      }

      const reader = response.body.getReader();
      const firstChunk = await reader.read();

      if (firstChunk.done) {
        throw new Error("Lege stream ontvangen");
      }

      if (winnerResolved) {
        reader.cancel();
        controller.abort();
        throw new Error("Andere call won de race");
      }

      winnerResolved = true;

      if (isHedge) {
        primaryController.abort();
      } else {
        hedgeController.abort();
      }

      return new ReadableStream({
        async start(streamController) {
          streamController.enqueue(firstChunk.value);
          try {
            while (true) {
              const { done, value } = await reader.read();
              if (done) {
                streamController.close();
                break;
              }
              streamController.enqueue(value);
            }
          } catch (err) {
            streamController.error(err);
          } finally {
            clearTimeout(timeoutId);
          }
        },
        cancel() {
          reader.cancel();
        }
      });
    } catch (err) {
      if (!winnerResolved && isHedge) {
        throw err;
      }
      throw err;
    }
  };

  return new Promise((resolve, reject) => {
    let primaryFailed = false;
    let hedgeStarted = false;

    executeCall(primary, primaryController, false)
      .then((stream) => {
        clearTimeout(hedgeTimerId);
        resolve(stream);
      })
      .catch((err) => {
        primaryFailed = true;
        if (!hedgeStarted) {
          executeHedge();
        }
      });

    const executeHedge = () => {
      if (winnerResolved || hedgeStarted) return;
      hedgeStarted = true;
      executeCall(hedge, hedgeController, true)
        .then(resolve)
        .catch((err) => {
          if (primaryFailed) {
            clearTimeout(timeoutId);
            reject(new Error("Zowel primaire als hedged aanroep gefaald"));
          }
        });
    };

    const hedgeTimerId = setTimeout(executeHedge, hedgeDelayMs);
  });
}

De keerzijde: dubbele tokenkosten en facturatie

Het grootste nadeel van hedging bij LLM-API's ten opzichte van traditionele RPC-calls is het financiële afrekenmodel. LLM-aanbieders factureren op basis van verwerkte invoertokens (prefill) en gegenereerde uitvoertokens (decode). Zodra een hedged aanroep de server van een provider bereikt en deze start met de prefill-fase, worden die invoertokens in rekening gebracht — zelfs als de verbinding 50 milliseconden later wordt verbroken met een client-side abort.

De financiële impact laat zich eenvoudig modelleren: bij een hedging-drempel ingesteld op de p90 wordt voor 10% van alle binnenkomende verzoeken een tweede call opgestart. Als de promptgrootte 4.000 tokens bedraagt, stijgen de totale prefill-kosten voor het systeem met circa 10%. Bij taken met enorme contextvensters (zoals RAG met 50.000+ tokens) kan dit leiden tot honderden euro's aan onbenutte prefill-kosten per dag. Om te voorkomen dat onverwachte hedging-golven het maandbudget opmaken, kun je harde kostenlimieten afdwingen via een budgetcap of kill switch in de routinglaag.

Om deze kosten binnen de perken te houden, worden drie strategieën toegepast:

Idempotentie en neveneffecten bij non-streaming taken

Wanneer LLM-calls worden ingezet voor function calling of geautomatiseerde mutaties in databases (zoals het aanmaken van records of versturen van e-mails), introduceert hedging een serieus risico op dubbele executie. Als twee instanties parallel een tool call genereren met identieke parameters, mag de backend deze actie niet tweemaal uitvoeren.

Voor niet-idempotente interacties moet het systeem strikte controles bevatten; lees hierover meer in het artikel over idempotentie bij LLM-API-calls om dubbele mutaties te voorkomen. Elke hedged request moet voorzien zijn van een unieke Client-Request-ID of idempotency-sleutel. Indien beide LLM-antwoorden toch doorkomen doordat annulering net te laat plaatsvond, zorgt de gateway ervoor dat slechts één resultaat aan de applicatielaag wordt overhandigd en verwerkt.

Wanneer moet je hedged requests juist NIET gebruiken?

Hoewel hedged requests een krachtig wapen zijn tegen trage staartresponstijden, zijn er scenario's waarin het patroon averechts werkt of zelfs gevaarlijk is voor de stabiliteit van de infrastructuur:

1. Tijdens algemene provider-storingen (Cascading Failures): Als een provider een wereldwijde overbelasting doormaakt en alle responses vertragen, zorgt hedging ervoor dat de gateway 10% tot 50% méér verkeer naar dat overbelaste cluster stuurt. Dit verergert de storing en leidt direct tot 429 Too Many Requests fouten. Om cascade-instabiliteit te stoppen kun je circuit breakers implementeren voor instabiele LLM-APIs zodat trage endpoints tijdelijk worden geïsoleerd.

2. Krappe Rate Limits (TPM / RPM): Als je API-tier strak tegen de limiet van het aantal tokens per minuut (TPM) aan zit, zal een piek in hedged requests direct leiden tot rate-limit uitputting voor regulier verkeer.

3. Asynchrone achtergrondverwerking: Voor data-extractie, offline samenvattingen of nachtelijke evaluaties is de p99-latency volkomen irrelevant; hier telt uitsluitend de kostenefficiëntie per token.

Conclusie en checklist voor productie

Hedged requests transformeren onvoorspelbare LLM-integraties tot betrouwbare bouwstenen met strakke SLA-grenzen voor eindgebruikers. Door selectief te vuren op basis van historische p90/p95 TTFT-statistieken en onmiddellijk te annuleren bij het eerste binnenkomende streaming chunk, kan de tail latency met tientallen procenten worden teruggedrongen zonder dat de kosten verdubbelen.

Controleer vóór uitrol in productie altijd de volgende randvoorwaarden: