Risposta diretta
Un solo numero di rate limit per tutta l'API è un errore da principiante. Il backend di questo portfolio usa quattro bucket Bucket4j separati: 120 richieste/secondo per l'API generica, 6 richieste/10 secondi per gli endpoint AI, 30/minuto per un download sensibile, 5/ora per l'emissione token. Un endpoint economo scoperto ha comunque prosciugato il bucket sbagliato in produzione: un flood di CSP report ha esaurito la quota condivisa e bloccato la verifica email di utenti legittimi con 429.
"Rate limit: 100 richieste al minuto" è la frase più pigra che si possa scrivere in una spec API. Un singolo numero uguale per ogni endpoint tratta allo stesso modo una GET che legge una riga da cache e una POST che invoca un modello LLM locale con centinaia di millisecondi di latenza. Nel backend di questo portfolio i bucket Bucket4j sono quattro, non uno, e persino con quattro bucket ben progettati ho comunque avuto un incidente in produzione dove un endpoint a bassa priorità ha prosciugato la quota di uno critico.
Quattro bucket, quattro decisioni di prodotto diverse
Il rate limiting non è un problema tecnico isolato, è una decisione di
prodotto travestita da configurazione. Nel backend, RateLimitConfig
definisce quattro policy con semantica distinta, ciascuna per un
motivo di business specifico:
- API generica: 120 richieste/secondo, refill 2 token/s per IP. Generosa, perché copre la stragrande maggioranza degli endpoint economici (lettura articoli, catalogo tool, ricerca).
- AI: 6 richieste/10 secondi, refill 1 token/10s per IP. Stretta di un ordine di grandezza, perché ogni richiesta invoca Ollama on-prem e consuma risorse computazionali reali sulla stessa VPS che serve anche il traffico applicativo.
- Download report analyzer: 30 richieste/minuto per IP, bucket dedicato. Introdotto dopo aver notato che il download di un report protetto solo da un UUID v4 casuale rischiava di ereditare o il bucket AI troppo stretto (falsi 429 su download legittimi) o il bucket API troppo largo (poca protezione contro il probing).
- Token di download Knowledge Pack: 5 token/ora, per userId autenticato, non per IP. L'unico dei quattro bucket keyed sull'identità invece che sull'indirizzo di rete.
| Bucket | Capacità | Refill | Chiave |
|---|---|---|---|
| API generica | 120 | 2 token/s | IP client |
| AI (Ollama) | 6 | 1 token/10s | IP client |
| Download report analyzer | 30 | 30 token/min | IP client |
| Token download Knowledge Pack | 5 | 5 token/h | userId |
RateLimitConfig.java, backend Spring Boot 3.4 di produzione. Store Caffeine, max 10.000 chiavi, eviction dopo 1h di inattività.Il routing verso il bucket giusto è dove nasce il bug
Avere quattro bucket non basta, serve anche instradare ogni richiesta
verso il bucket corretto, e qui il codice si complica rapidamente. Il
backend classifica un percorso come "AI" non solo per prefisso
(/api/v1/ai/**), ma con una lista esplicita di path
fiscali che condividono il bucket AI perché chiamano Ollama
(/fiscal/regime/compare, /fiscal/chat,
/business-tools/ai-boost, gli analyzer settoriali,
il trip planner), più regole per substring su path dinamici di
ProjectHub (/project-hub/workspaces/{slug}/ai/*). Ogni
volta che aggiungo un nuovo endpoint che chiama Ollama, devo ricordarmi
di aggiungerlo esplicitamente a questa lista, altrimenti finisce nel
bucket generico da 120 richieste/secondo e diventa un vettore di abuso
economico sull'inferenza AI mascherato da traffico API normale.
Il caso inverso, altrettanto insidioso, è quello del download report:
inizialmente ereditava il bucket AI per errore di categorizzazione di
path (prefisso condiviso), col risultato che utenti legittimi che
scaricavano il proprio report ottenevano 429 dopo pochi tentativi,
perché consumavano la stessa quota scarsissima riservata alle chiamate
Ollama. La correzione (CR-01 nel codice) esclude
esplicitamente quel path prima ancora di controllare se inizia con
/api/v1/ai, e gli assegna un bucket dedicato da 30/minuto,
né troppo stretto né troppo largo per il caso d'uso.
Postmortem: quando il bucket sbagliato blocca l'endpoint giusto
Il caso più istruttivo in produzione non ha riguardato l'abuso
intenzionale, ma un comportamento legittimo del browser che ha
prosciugato una risorsa condivisa. Durante il rollout della Content
Security Policy in modalità Report-Only, ogni pagina prerenderizzata
caricata dal browser generava circa 20 richieste POST /csp-report
per singolo page-load (violazioni CSP attese, non malevoli). Quelle
richieste condividevano lo stesso bucket API generico da 120/s usato
anche dall'endpoint di verifica email. Il flood di CSP report ha
esaurito la quota IP-scoped abbastanza da far scattare 429 anche sulle
richieste legittime di verifica email dello stesso client. Il fix è
stato escludere /csp-report dal rate limiter, perché è
un endpoint di telemetria del browser, non un endpoint applicativo, e
non deve mai competere per la stessa quota con azioni utente reali.
La lezione generale, non specifica al CSP: qualunque endpoint che il browser chiama automaticamente, senza un'azione utente esplicita (telemetria, beacon, report), va tenuto fuori dal rate limiting condiviso con endpoint azionati dall'utente, oppure va isolato in un bucket dedicato. Se condivide la quota con un'azione utente reale, il volume automatico del browser vince sempre, e l'utente reale paga il prezzo con un errore che non ha causato.
Trade-off: IP-based versus identity-based
Tre bucket su quattro sono keyed per IP, uno per userId. Il keying per IP ha un limite noto: dietro NAT aziendali o reti mobili CGNAT condivise, molti utenti legittimi possono condividere lo stesso IP e quindi lo stesso bucket, aumentando il rischio di 429 su traffico innocuo aggregato. Il keying per userId richiede invece autenticazione già avvenuta, quindi non è utilizzabile su endpoint pubblici pre-login (come login stesso, o verifica email). La scelta pratica nel backend è stata IP-based ovunque tranne dove l'endpoint richiede già un utente autenticato con un'azione ad alto valore (emissione di un token di download): lì l'identità è un segnale più preciso dell'IP e vale la complessità aggiuntiva.
Quando un solo bucket generico basta davvero
Non ogni backend ha bisogno di quattro bucket. Se non esiste alcun endpoint che invoca un modello AI o un'operazione costosa lato server, un singolo bucket generico ben dimensionato è sufficiente e la complessità di routing multi-bucket è ingiustificata. Il vero segnale che serve differenziare è la presenza di endpoint con costo computazionale o di rete asimmetrico rispetto al resto dell'API, non il numero assoluto di endpoint esposti.
Domande frequenti
Perché un'API dovrebbe avere più bucket di rate limit invece di uno solo?
Endpoint diversi hanno costi diversi. Un endpoint che invoca un modello AI consuma risorse computazionali reali molto più di una lettura da cache, quindi merita una quota molto più stretta. Un singolo bucket condiviso tratta tutti gli endpoint allo stesso modo e non protegge le risorse più costose dall'abuso.
Cosa può causare un flood di CSP report contro il rate limiter?
Durante il rollout di una Content Security Policy in modalità Report-Only, ogni pagina caricata genera automaticamente diverse richieste POST verso l'endpoint di report per ogni violazione CSP osservata dal browser, anche se attesa e non malevola. Se questo endpoint condivide il bucket con endpoint applicativi reali, può esaurirne la quota.
Come si evita che un endpoint di telemetria del browser blocchi endpoint utente reali?
Escludendo esplicitamente gli endpoint di telemetria automatica (report CSP, beacon, analytics) dal rate limiter condiviso, oppure assegnando loro un bucket dedicato separato. Un endpoint chiamato automaticamente dal browser genera un volume che non riflette l'intento di un utente reale, e non deve competere per la stessa quota.
Meglio il rate limiting per IP o per identità utente (userId)?
Dipende dal contesto. Il rate limiting per IP funziona ovunque, anche pre-login, ma soffre dietro NAT condivisi dove più utenti reali usano lo stesso indirizzo. Il rate limiting per userId è più preciso ma richiede autenticazione già avvenuta, quindi è adatto solo ad azioni ad alto valore compiute da utenti già loggati.
Quando basta un solo bucket di rate limit generico per tutta l'API?
Quando non esistono endpoint con un costo computazionale o di rete significativamente più alto del resto dell'API, come chiamate a modelli AI o operazioni di elaborazione pesante lato server. In quel caso, un singolo bucket ben dimensionato è sufficiente e il routing multi-bucket aggiunge complessità senza benefici reali.
Commenti
Caricamento commenti...
Accedi per commentare