La maggior parte delle implementazioni di limitazione delle richieste API vengono banalmente aggirate da una rotazione ingenua dei proxy — ma quella stessa rotazione attiva euristiche anti-abuso in pochi minuti. La vera sfida non è solo rimanere sotto il limite, ma farlo senza sembrare un bot. Ciò richiede la comprensione dei due distinti ambiti di limitazione (per-IP e per-chiave), l'applicazione di un backoff con jitter, la randomizzazione della rotazione del pool e la modellazione dei tempi di richiesta per imitare il traffico organico. Il pattern del token bucket, stratificato su un pool di proxy, fornisce una base solida.
Per-IP vs Per-Chiave: I Due Assi della Limitazione delle Richieste
I limiti di richiesta operano su almeno due dimensioni indipendenti: l'IP di origine e la chiave API (o token). Una singola chiave può consentire 5.000 richieste all'ora (limite autenticato di GitHub), ma la stessa chiave da un singolo IP potrebbe essere limitata con un tetto di burst inferiore. Le richieste non autenticate sono ancora più vincolate — tipicamente 60 richieste all'ora per IP. Ignorare la dimensione IP è il modo più rapido per innescare una risposta 429 Too Many Requests (RFC 6585). Un pool di proxy deve distribuire le richieste su più IP per evitare di saturare una singola origine, ma ogni IP condivide comunque la stessa chiave. Se il limite globale della chiave è 5.000/ora e hai 50 proxy, ogni proxy può effettuare solo 100 richieste all'ora prima che scatti il contatore a livello di chiave. Mappa entrambi i limiti prima di scrivere una singola riga di codice.
Backoff con Jitter: La Differenza tra Educato e Prevedibile
Il backoff esponenziale senza jitter è un'impronta digitale. I server vedono richieste che arrivano a intervalli perfettamente raddoppiati e le contrassegnano come automatizzate. La soluzione è il jitter completo: sleep(random.uniform(0, min(cap, base * 2 ** attempt))). Questo distribuisce i tentativi nell'arco della finestra temporale, rendendo il pattern indistinguibile da un picco di utenti reali. La documentazione di AWS sul backoff esponenziale raccomanda il jitter proprio per questo motivo. Il seguente snippet Python implementa un token bucket che si ricarica a un ritmo fisso, si mette in pausa con jitter quando è vuoto e ruota il proxy a ogni acquisizione riuscita:
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
Rotazione del Pool di Proxy: Randomizza, Non Round-Robin
Il round-robin sequenziale attraverso un elenco di proxy è rilevabile. Un server che vede richieste da 1.2.3.4, poi 5.6.7.8, poi 9.10.11.12 in perfetta sincronia correlerebbe il pattern tra gli IP. Invece, scegli i proxy in modo casuale con reinserimento e aggiungi un periodo di raffreddamento per proxy. Le directory pubbliche di proxy riportano tassi di fallimento del 60–80% — un proxy che restituisce un 429 o un errore di connessione dovrebbe essere declassato a una coda morta per almeno 60 secondi. Implementa una selezione casuale pesata in cui i proxy recentemente riusciti hanno maggiori probabilità di essere scelti. Il token bucket sopra utilizza un semplice rotate(1) per chiarezza; in produzione sostituiscilo con random.choice() e tieni traccia dei conteggi di fallimento per proxy.
Modellazione delle Richieste: Imita la Media Senza Superare il Limite
Anche con un token bucket e un pool di proxy perfetti, un tasso di richiesta costante di esattamente 2,0 al secondo è innaturale. Gli utenti reali fanno pause, raggruppano e variano gli intervalli tra le richieste. Aggiungi un piccolo ritardo casuale (ad es., time.sleep(random.uniform(0.1, 0.5))) prima di ogni chiamata acquire() e varia il tasso del token bucket del ±10% ogni pochi minuti. Tuttavia, non aggiungere ritardi che superino la finestra di limitazione documentata dell'API — ciò vanificherebbe lo scopo. L'obiettivo è rimanere entro il limite apparendo come un insieme di client legittimi. Una modellazione eccessiva (ad es., inserire ritardi di digitazione simili a quelli umani) è uno sforzo sprecato; all'API interessa la frequenza delle richieste, non la tempistica tra i tasti.
Mitigazione Etica: Sapere Dove è il Limite
Ogni tecnica descritta qui è un'ottimizzazione legittima — finché non viola i Termini di Servizio dell'API. La maggior parte dei ToS vieta esplicitamente di "aggirare i limiti di richiesta" o "utilizzare mezzi automatizzati per accedere al servizio". Leggere i ToS non è facoltativo. Se l'API consente 100 richieste al minuto per chiave, utilizzare 50 proxy ciascuno che effettua 2 richieste al minuto è conforme. Utilizzare 100 proxy ciascuno che effettua 100 richieste al minuto non lo è — è abuso, indipendentemente da quanto sia intelligente il tuo jitter. La distinzione è l'intento e il volume. Un token bucket stratificato su un pool di proxy è uno strumento; lo stesso strumento che raschia eticamente una directory pubblica può anche essere usato per fare DDoS a una piccola API. Documenta i tuoi obiettivi di limitazione, controlla i tuoi log per 429 e non superare mai il tetto documentato per chiave. Se hai bisogno di più throughput, chiedi un limite più alto o paga per un piano dedicato.