Naar de inhoud
NLEN
Illustratie: Distributed rate limiting over meerdere API-instances

Distributed rate limiting over meerdere API-instances

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

Wanneer een LLM-infrastructuur groeit van een enkele server naar een gedistribueerde cluster met tientallen API-instances, verliest een lokaal rate-limiting-mechanisme zijn effectiviteit. Als individuele workers hun eigen tellers bijhouden in het lokale geheugen, kan een piek in inkomende verzoeken alsnog upstream rate limits overschrijden of tot oneerlijke toewijzing tussen tenants leiden. Binnen architectuurpijler A2 vormt centrale coördinatie het fundament; lees de gids over zelf een LLM-gateway hosten om te zien hoe proxy-lagen en failover-clusters fundamenteel worden opgezet.

In dit artikel analyseren we hoe gedistribueerde rate limiting in een multi-instance gateway werkt. We behandelen de specifieke dimensies van LLM-beperkingen (requests per minuut, tokens per minuut en concurrency), de onderliggende datastructuren en Redis Lua-scripts, technieken voor lokale batchreserveringen om roundtrips te minimaliseren, en de trade-offs tussen absolute synchronisatie en doorvoersnelheid.

De unieke uitdaging van rate limiting bij LLM-gateways

Klassieke HTTP API rate limiters meten hoofdzakelijk Requests Per Minute (RPM). Bij taalmodellen volstaat deze enkele dimensie niet. Upstream modelproviders (zoals Anthropic, OpenAI of zelf-gehoste vLLM-clusters) hanteren drie verschillende, simultane begrenzingen die onafhankelijk van elkaar kunnen vollopen:

De asymmetrie van tokenverbruik maakt gedistribueerde synchronisatie complex. Bij aanvang van een API-call is de omvang van de invoer bekend, maar de uitvoerlengte is onzeker tot de response volledig is gegenereerd of afgebroken. Om het risico op overschrijding te beheersen, moeten we het mechanisme koppelen aan de grondbeginselen van doorvoerbeheer; bekijk de analyse over het token bucket algoritme in een LLM-gateway voor de wiskundige eigenschappen van continue navulling en burst-tolerantie.

Dimensie Meetmoment Onzekerheid vooraf Faalmodus bij desynchronisatie
RPM Start van het verzoek Geen (tellertoename = 1) HTTP 429 van upstream provider
TPM Start (schatting) + Einde (correctie) Hoog (onbekende outputlengte) Plotselinge throttling over alle instances
Concurrency Start (lock/slot) + Einde (release) Tijdsduur streaming onbekend Resource-uitputting op model-backends

Architectuurpatronen voor gedistribueerde coördinatie

Er zijn drie hoofdpatronen om de verbruiksstaat tussen onafhankelijke API-instances synchroon te houden. Elk patroon brengt eigen concessies met zich mee op het gebied van netwerklatency, consistentie en complexiteit.

1. Centrale State Store (Redis / Dragonfly)

Elke inkomende request forceert een snelle remote operatie naar een centrale in-memory datastore. Omdat meerdere instances gelijktijdig dezelfde teller proberen bij te werken, worden updates verpakt in atomaire Lua-scripts. Dit voorkomt race conditions (zoals check-then-act fouten), maar introduceert een extra netwerk-roundtrip (doorgaans 0.5 tot 2.0 milliseconden in hetzelfde datacenter) vóórdat de gateway het verzoek doorstuurt naar de modelprovider.

2. Batch Allocation / Token Leasing

In plaats van per individueel verzoek een centrale store te raadplegen, reserveert elke API-instance periodiek een "lease" of "tokenblok" uit de centrale pool (bijvoorbeeld 50 verzoeken en 50.000 tokens voor 5 seconden). De instance handelt binnenkomend verkeer lokaal en zonder netwerklatency af zolang het lease-budget niet is uitgeput. Ongebruikte tokens worden na afloop van de lease-periode teruggestort.

3. Consistent Hashing op de Load Balancer

De load balancer routeert inkomend verkeer op basis van de hash van de tenant-ID of API-sleutel naar altijd dezelfde gateway-instance. Hierdoor kan de rate limiting volledig lokaal in het RAM van de instance plaatsvinden. Het nadeel is dat bij het toevoegen of wegvallen van instances de hashes herverdeeld worden, en dat "hot tenants" één instance onevenredig zwaar kunnen belasten.

Implementatie: Atomair Sliding Window in Redis via Lua

Een glijdend venster (sliding window log of sliding window counter) voorkomt de bekende grensoverschrijdingen bij vaste vensters, waarbij een dubbele burst rondom de minuutwisseling kan optreden. In Redis implementeren we dit via een geordende set (Sorted Set, ZSET), waarin de score en het lid beide bestaan uit het timestamp in milliseconden.

-- Lua script voor gedistribueerde sliding window rate limiting
-- KEYS[1]: Redis key (bijv. "ratelimit:tpm:tenant_123")
-- ARGV[1]: Huidige timestamp in milliseconden
-- ARGV[2]: Venstergrootte in milliseconden (bijv. 60000)
-- ARGV[3]: Maximale capaciteit (bijv. 100000 tokens)
-- ARGV[4]: Gevraagde eenheden voor dit verzoek (bijv. 1200 tokens)
-- ARGV[5]: Unieke request identifier

local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local max_limit = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
local req_id = ARGV[5]

local clear_before = now - window

-- 1. Verwijder records die buiten het glijdende venster vallen
redis.call('ZREMRANGEBYSCORE', key, '-inf', clear_before)

-- 2. Bereken de huidige som van het verbruik
local entries = redis.call('ZRANGE', key, 0, -1, 'WITHSCORES')
local current_usage = 0
for i = 1, #entries, 2 do
  -- Het element bevat "cost:req_id"
  local raw_val = entries[i]
  local sep = string.find(raw_val, ":")
  if sep then
    local item_cost = tonumber(string.sub(raw_val, 1, sep - 1))
    current_usage = current_usage + (item_cost or 0)
  end
end

-- 3. Valideer of er voldoende ruimte is
if current_usage + cost <= max_limit then
  -- Voeg het huidige verbruik toe aan de ZSET
  local member = tostring(cost) .. ":" .. req_id
  redis.call('ZADD', key, now, member)
  -- Stel TTL in op iets meer dan het venster voor automatische opruiming
  redis.call('PEXPIRE', key, window + 1000)
  return {1, max_limit - (current_usage + cost), 0} -- Toegestaan
else
  -- Bereken wachttijd tot er voldoende capaciteit vrijkomt
  local oldest_entries = redis.call('ZRANGE', key, 0, 1, 'WITHSCORES')
  local retry_after = 0
  if #oldest_entries >= 2 then
    retry_after = math.max(0, (tonumber(oldest_entries[2]) + window) - now)
  end
  return {0, max_limit - current_usage, retry_after} -- Geweigerd
end

Dit script voert de opschoning, berekening en toekenning in één atomair blok uit op de Redis-server. Omdat Redis single-threaded opereert ten aanzien van command-uitvoering, kunnen twee API-instances op hetzelfde milliseconde-interval nooit een inconsistente staat waarnemen of overschrijden.

Twee-fase Token Reservering bij Onbekende Completion Lengte

Bij het doorsturen van een prompt naar een LLM weten we hoeveel tokens de prompt bevat ($T_{in}$), maar niet hoeveel tokens het antwoord zal genereren ($T_{out}$). Wanneer men pas achteraf meet, kan een gelijktijdige batch van lange antwoorden de upstream TPM-limiet overschrijden. Registreert men vooraf conservatief de maximale modelcontext (`max_tokens`), dan worden legitieme verzoeken ten onrechte afgewezen (valse 429-fouten).

De oplossing hiervoor is een twee-fase reserveringsprotocol (Two-Phase Token Reservation):

Fase 1: Reservering vooraf
De API-instance berekent $T_{in}$ via een lokale tokenizer en maakt een inschatting van $T_{est} = T_{in} + \min(\text{max\_tokens}, \text{historisch\_gemiddelde} \times 1.5)$. De instance reserveert $T_{est}$ tokens in de centrale Redis-limiter via het atomair script.

Fase 2: Afrekening en afstemming achteraf
Nadat de response via Server-Sent Events (SSE) of standaard JSON is voltooid, leest de instance het daadwerkelijk gerapporteerde aantal tokens ($T_{actueel} = T_{in} + T_{out}$) uit de payload van de provider. Het script voert een reconciliatie uit:

Voor inzicht in hoe upstream quota en financiële limieten zich tot deze technische buffers verhouden, raadpleegt men het artikel over rate limits, tokens en kosten om overschrijdingen op organisatieniveau te voorkomen.

Concurrency Beperking met Gedistribueerde Semaphores

Naast tijdgebonden limieten (RPM/TPM) is concurrency-beperking cruciaal om te voorkomen dat streaming verbindingen alle beschikbare sockets of vLLM batching-slots bezet houden. Een gedistribueerde semaphore via Redis bewaakt het aantal gelijktijdig actieve verbindingen over alle instances.

import time
import uuid
import redis

class DistributedConcurrencyLimiter:
    def __init__(self, redis_client: redis.Redis, resource_key: str, max_concurrent: int, timeout_sec: int = 120):
        self.r = redis_client
        self.key = f"semaphore:{resource_key}"
        self.max_concurrent = max_concurrent
        self.timeout_sec = timeout_sec

    def acquire(self) -> str | None:
        """Probeert een slot te reserveren. Retourneert slot_id bij succes, anders None."""
        slot_id = str(uuid.uuid4())
        now = time.time()
        
        # Ruim verouderde slots op (bijvoorbeeld door gecrashte instances)
        pipe = self.r.pipeline()
        pipe.zremrangebyscore(self.key, '-inf', now - self.timeout_sec)
        pipe.zcard(self.key)
        _, current_count = pipe.execute()

        if current_count < self.max_concurrent:
            # Voeg slot toe met huidige timestamp als score
            added = self.r.zadd(self.key, {slot_id: now}, nx=True)
            if added:
                return slot_id
        return None

    def release(self, slot_id: str) -> None:
        """Geeft het gereserveerde slot direct vrij."""
        self.r.zrem(self.key, slot_id)

Een harde les uit productie-omgevingen is het risico op "spook-slots": wanneer een API-instance crasht of een netwerkonderbreking ervaart terwijl een streaming response loopt, mag het slot niet oneindig bezet blijven. Het opruimen van entries ouder dan timeout_sec bij elke `acquire`-operatie voorkomt dat een dode verbinding de totale cluster-concurrency permanent verlaagt.

Pre-fetching en Local Token Buckets bij Hoge Doorvoer

Bij duizenden requests per seconde creëert een centrale store een latency-bottleneck. Als elke gateway-node 100 verzoeken per seconde afhandelt, veroorzaken tien nodes samen 1.000 Redis-aanroepen per seconde per tenant. Om dit te reduceren passen we een hybride strategie toe: Local Leased Token Buckets.

Hierbij vraagt een API-instance periodiek een blok tokens op bij de centrale Redis-instantie. Zodra de instance een lokaal budget heeft van bijvoorbeeld 20.000 tokens, valideert en verwerkt hij binnenkomende calls volledig in het lokale werkgeheugen (latency < 0.05ms). Pas wanneer de lokale buffer onder 20% van de capaciteit zakt, stuurt de achtergrondthread een asynchrone vernieuwingsaanvraag naar de centrale Redis-server.

Zwak punt van Local Token Leases: Als het verkeer onevenredig verdeeld is over de instances (bijvoorbeeld 90% van de traffic landt op instance A door sticky routing), kan instance A door zijn lokale pool heen raken en verzoeken onterecht afwijzen, terwijl instance B en C duizenden ongebruikte tokens in hun lokale lease vasthouden. Stel de lease-duur daarom kort in (1 tot 3 seconden) en implementeer een direct teruggavemechanisme bij inactiviteit.

Queueing en Backpressure bij Uitputting

Wanneer een rate limit bereikt wordt, is het direct teruggeven van een HTTP 429 statuscode vaak ongewenst voor de eindgebruiker of de aanroepende pipeline. Een robuuste architectuur combineert rate limiting met een gedistribueerde wachtrij. In plaats van verzoeken weg te gooien, worden ze geparkeerd in een prioriteitsbuffer tot er weer capaciteit beschikbaar komt.

Om te voorkomen dat interactieve gebruikersinteracties worden geblokkeerd door zware achtergrond-taken, splitsen we de wachtrij op basis van urgentie; lees de documentatie over prioriteitswachtrijen voor LLM-taken voor implementatiepatronen rondom latency-budgetten en starvation-preventie.

Voor een breder overzicht van open-source en enterprise tooling die deze logica modulair kan aanbieden, biedt de gids voor kostenbewaking en token management voor LLM-applicaties vergelijkend inzicht in kant-en-klare gateway-oplossingen versus maatwerkbouw.

Fouttolerantie en Fail-Open vs. Fail-Closed Strategieën

Wat gebeurt er als de centrale Redis-cluster onbereikbaar wordt of een netwerkpartitie ervaart? In de gateway-architectuur moet expliciet worden gekozen tussen twee gedragslijnen:

Strategie Gedrag bij Redis-uitval Voordeel Risico
Fail-Open Verzoeken worden doorgelaten zonder centrale controle. Dienstverlening blijft beschikbaar voor eindgebruikers. Kostenpieken en cascade-fouten door upstream 429's.
Fail-Closed Alle verzoeken die centrale verificatie vereisen worden geweigerd (HTTP 503/429). Gegarandeerde budgetbeveiliging en geen contractbreuk met providers. Totale downtime van de applicatie bij storing in de cachinglaag.
Degraded Local Fallback Schakel over naar een lokaal, conservatief noodbudget per instance (bijv. 20% van normaal). Beperkte beschikbaarheid zonder risico op massale overload. Complexere statusovergangen en kans op korte oneerlijke throttling.

In de praktijk blijkt Degraded Local Fallback de meest stabiele keuze voor productie-omgevingen: wanneer de centrale store niet binnen een ingestelde timeout van 15 milliseconden reageert, valt de instance tijdelijk terug op een strikte lokale in-memory teller totdat de centrale verbinding hersteld is.

Samenvatting en Besturingsregels

Gedistribueerde rate limiting over meerdere API-instances vereist een zorgvuldige balans tussen nauwkeurigheid, latency en foutbestendigheid. Om het systeem stabiel te houden in productie hanteren we de volgende vuistregels: