API Engineering

API-rate limits mitigeren zonder anti-abuse heuristieken te activeren

4 min read Published Updated 808 words

De meeste implementaties van API-rate limits worden eenvoudig omzeild door naïeve proxyrotatie — maar diezelfde rotatie activeert binnen enkele minuten anti-misbruikheuristieken. De echte uitdaging is niet alleen onder de limiet blijven, maar dat doen zonder eruit te zien als een bot. Dit vereist inzicht in de twee verschillende schalen van snelheidsbeperking (per-IP en per-sleutel), het toepassen van jittered backoff, het randomiseren van poolrotatie en het vormgeven van de timing van verzoeken om organisch verkeer na te bootsen. Het token bucket-patroon, bovenop een proxypool, biedt een robuuste basis.

Per-IP versus Per-Key: De twee assen van snelheidsbeperking

Rate limits werken op ten minste twee onafhankelijke dimensies: het bron-IP en de API-sleutel (of token). Een enkele sleutel mag bijvoorbeeld 5.000 verzoeken per uur toestaan (GitHub's geauthenticeerde limiet), maar dezelfde sleutel vanaf een enkel IP kan worden beperkt met een lager burst-plafond. Niet-geauthenticeerde verzoeken zijn nog meer beperkt — doorgaans 60 verzoeken per uur per IP. Het negeren van de IP-dimensie is de snelste manier om een 429 Too Many Requests (RFC 6585)-respons te krijgen. Een proxypool moet verzoeken over meerdere IP's verdelen om te voorkomen dat één enkele oorsprong verzadigd raakt, maar elk IP deelt nog steeds dezelfde sleutel. Als de globale limiet van de sleutel 5.000/uur is en je hebt 50 proxies, kan elke proxy slechts 100 verzoeken per uur doen voordat de teller op sleutelniveau afgaat. Breng beide limieten in kaart voordat je ook maar één regel code schrijft.

Backoff met Jitter: Het verschil tussen beleefd en voorspelbaar

Exponentiële backoff zonder jitter is een vingerafdruk. Servers zien verzoeken aankomen met perfect verdubbelende intervallen en markeren ze als geautomatiseerd. De oplossing is volledige jitter: sleep(random.uniform(0, min(cap, base * 2 ** attempt))). Dit spreidt herpogingen over het tijdsvenster, waardoor het patroon niet te onderscheiden is van een uitbarsting van echte gebruikers. AWS's eigen documentatie over exponentiële backoff beveelt jitter aan om precies deze reden. Het volgende Python-fragment implementeert een token bucket die met een vast tempo wordt bijgevuld, met jitter slaapt wanneer deze leeg is, en bij elke succesvolle acquisitie de proxy roteert:

import time
import random
from collections import deque

class TokenBucketProxyPool:
    def __init__(self, proxies, rate, burst):
        self.proxies = deque(proxies)
        self.rate = rate          # tokens per second
        self.burst = burst
        self.tokens = burst
        self.last_refill = time.monotonic()

    def refill(self):
        now = time.monotonic()
        elapsed = now - self.last_refill
        self.tokens = min(self.burst, self.tokens + elapsed * self.rate)
        self.last_refill = now

    def acquire(self):
        while True:
            self.refill()
            if self.tokens >= 1:
                self.tokens -= 1
                self.proxies.rotate(1)   # simple rotation, but see section below
                return self.proxies[0]
            else:
                sleep_time = (1 - self.tokens) / self.rate
                jitter = random.uniform(0, sleep_time * 0.5)
                time.sleep(sleep_time + jitter)

# Usage: pool = TokenBucketProxyPool(proxies, rate=2.0, burst=10)
#         proxy = pool.acquire()   # blocks until token available

Proxy Pool Rotatie: Randomiseer, geen Round-Robin

Sequentiële round-robin door een proxy-lijst is detecteerbaar. Een server die verzoeken ziet van 1.2.3.4, vervolgens 5.6.7.8, vervolgens 9.10.11.12 in lockstep, zal het patroon correleren over IP's heen. Kies in plaats daarvan willekeurig proxies met teruglegging en voeg een per-proxy cooldown toe. Openbare proxy-directory's rapporteren 60–80% foutpercentages — een proxy die een 429 of een verbindingsfout retourneert, moet worden gedegradeerd naar een dode wachtrij gedurende ten minste 60 seconden. Implementeer een gewogen willekeurige selectie waarbij recent succesvolle proxies vaker worden gekozen. De token bucket hierboven gebruikt een eenvoudige rotate(1) voor de duidelijkheid; vervang dat in productie door random.choice() en houd per-proxy fouttellingen bij.

Request Shaping: Nabootsen van het gemiddelde zonder de grens te overschrijden

Zelfs met een perfecte token bucket en proxy pool is een constante verzoeksnelheid van precies 2.0 per seconde onnatuurlijk. Echte gebruikers pauzeren, bundelen en variëren de intervallen tussen verzoeken. Voeg een kleine willekeurige vertraging toe (bijv. time.sleep(random.uniform(0.1, 0.5))) vóór elke acquire()-aanroep en varieer de snelheid van de token bucket met ±10% om de paar minuten. Voeg echter geen vertragingen toe die het gedocumenteerde rate limit-venster van de API overschrijden — dat ondermijnt het doel. Het doel is om binnen de limiet te blijven terwijl je eruitziet als een cluster van legitieme clients. Over-shaping (bijv. het invoegen van menselijke typvertragingen) is verspilde moeite; de API geeft om verzoekfrequentie, niet om timing tussen toetsaanslagen.

Ethische mitigatie: Weet waar de grens ligt

Elke hier beschreven techniek is een legitieme optimalisatie — totdat deze de Terms of Service van de API schendt. De meeste ToS verbieden expliciet het 'omzeilen van rate limits' of 'het gebruik van geautomatiseerde middelen om toegang te krijgen tot de service'. Het lezen van de ToS is niet optioneel. Als de API 100 verzoeken per minuut per sleutel toestaat, is het gebruik van 50 proxies die elk 2 verzoeken per minuut doen, compliant. Het gebruik van 100 proxies die elk 100 verzoeken per minuut doen, is dat niet — het is misbruik, ongeacht hoe slim je jitter is. Het onderscheid is intentie en volume. Een token bucket bovenop een proxy pool is een hulpmiddel; hetzelfde hulpmiddel dat ethisch een openbare directory scrapet, kan ook worden gebruikt om een kleine API te DDoS'en. Documenteer je rate limit-doelen, controleer je logs op 429's en overschrijd nooit het gedocumenteerde per-sleutel plafond. Als je meer doorvoer nodig hebt, vraag dan om een hogere limiet of betaal voor een dedicated plan.