Rate limiting e cost governance per applicazioni AI: token budgeting, edge throttling, difesa anti-abuso
Il concetto di rate limiting per un'API tradizionale è stato risolto quindici anni fa: X-RateLimit-Remaining: 42, Retry-After: 60, Redis con sliding window o token bucket, HTTP 429. Su un'API AI la matematica è radicalmente diversa, e molti team se ne accorgono con una fattura Anthropic o OpenAI a quattro cifre arrivata in un weekend. La differenza è che una request HTTP ha un costo computazionale lineare e contenuto - stessa logica di autenticazione, stessa query al database, stessa serializzazione JSON - mentre una request a un endpoint AI può costare 400 token oppure 40.000 a seconda del contenuto dell'input e della verbosità dell'output. Contare le richieste non misura il costo. Contare i token sì. E se nel mezzo tra utente e modello non hai una pipeline di cost governance strutturata, hai solo un contatore difettoso che ti tranquillizza mentre il bill cresce.
Il 18 gennaio 2026 ho fatto un esperimento deliberato nella mia pipeline di ricerca applicata: ho esposto un endpoint pubblico (con autenticazione via API key ma senza rate limiting applicativo, solo Cloudflare Pro standard davanti) che riassumeva articoli di blog passati in input. Dopo 31 ore un crawler di terza parte - evidentemente alimentato automaticamente da un discovery feed - ha eseguito 14.800 chiamate consecutive al mio endpoint, consumando 11,4 milioni di token Claude Sonnet 4.5. Costo: 96,72€ in una giornata, su un budget mensile che per quel workload avevo dimensionato a 40€. Il mio circuit breaker di test è scattato al settimo minuto di throttling anomalo e ha interrotto il servizio, ma se lo stesso crawler avesse distribuito le richieste uniformemente sulle 24h con un pattern naïf (non a burst), il circuit breaker non sarebbe scattato e avrei visto il conto a fine giornata quando l'alert Anthropic mi ha notificato il 90% del budget mensile consumato. Il fatto che si chiami denial-of-wallet attack e non denial-of-service descrive esattamente il danno: il servizio è rimasto disponibile, è il mio portafoglio ad essere stato drenato.
Perché il rate limiting di un'API tradizionale non protegge un'API AI
La OWASP Top 10 for LLM Applications 2025 classifica questo scenario come LLM10 - Unbounded Consumption: "uso LLM non regolato che produce consumo eccessivo di risorse o denial-of-wallet attacks". È l'ultima voce della classifica per ranking ma la più insidiosa per due ragioni architetturali precise. La prima: il costo di una request non è prevedibile pre-esecuzione senza stimare i token. Se il tuo endpoint accetta "testo arbitrario dell'utente" in input, l'input stesso può contenere 100 o 100.000 caratteri, che si traducono in una forbice di costo di 1000x. La seconda: il costo marginale di un'evasione è minimo per l'attaccante. Scrivere uno script Python di 30 righe che chiama il tuo endpoint con payload massimi costa zero; la tua bolletta cresce in tempo reale.
Non basta quindi contare request: serve contare token consumati per utente autenticato, per finestra temporale, per ambito applicativo. E serve un sistema di quattro layer, ciascuno con una responsabilità isolata e un meccanismo di escalation indipendente. Il primo layer blocca gli abusi grezzi prima che raggiungano il tuo backend. Il secondo traccia il consumo reale per utente. Il terzo applica hard cap mensili con degrado controllato del servizio. Il quarto genera alert e log di audit per revisione post-incident. Saltare uno qualunque dei quattro significa avere un sistema che funziona in produzione finché non ti ritrovi un attaccante motivato, poi collassa.
Questo tipo di architettura è quello che approfondisco nel mio hub dedicato all'automazione AI per aziende, dove raccolgo articoli su cost tracking granulare, osservabilità delle chiamate LLM in produzione, pattern di fallback tra modelli con downshift selettivo. Il filo conduttore è sempre lo stesso: l'AI come componente di produzione, con la stessa disciplina che applicheresti a un servizio di pagamento - perché di fatto è un servizio che ti fa pagare.
Layer 1: edge throttling pre-applicativo con Cloudflare
La prima barriera è davanti al tuo backend. Qualsiasi request malformata, abusiva o palesemente automatizzata deve essere rigettata all'edge, senza mai consumare una riga di codice applicativo e senza mai far partire una chiamata API LLM. Cloudflare (o equivalente: Fastly, CloudFront+WAF, Caddy con rate limit module) è la scelta di default per una PMI che non vuole gestire un proprio WAF. La configurazione minima che applico prevede tre regole indipendenti in cascata.
La prima è il rate limiting per IP grezzo, più generoso, pensato per bloccare solo comportamenti palesemente automatizzati: 60 request al minuto per IP per endpoint, con block per 10 minuti oltre soglia. Questo filtra script casuali e scanner automatici, non utenti normali. La seconda è il rate limiting per Bearer token, più stretto: 600 request all'ora per token autenticato. Questo è il contatore primario per gli utenti reali. La terza è una challenge Turnstile scatenata da euristiche di Cloudflare (fingerprint browser sospetti, geo anomali, header non coerenti): non blocca, introduce un captcha invisibile che filtra bot sofisticati senza impattare utenti legittimi. Per un endpoint pubblico come una chat AI, la Turnstile challenge va messa sul layer pubblico (landing page, signup) e disabilitata sul layer autenticato per evitare frizione inutile.
# Cloudflare Rule 1: IP rate limit (bulk filter)
when (http.request.uri.path matches "^/api/ai/" and rate(1m) > 60)
then block for 10m
# Cloudflare Rule 2: Bearer token rate limit (primary counter)
when (http.request.uri.path matches "^/api/ai/"
and http.request.headers["authorization"][0] != ""
and rate_by(http.request.headers["authorization"][0], 1h) > 600)
then block for 15m
# Cloudflare Rule 3: Turnstile challenge on suspicious signal
when (http.request.uri.path matches "^/api/ai/"
and cf.threat_score > 30)
then challenge (managed)Cloudflare fornisce questi pattern in forma documentata nella propria documentazione ufficiale sul rate limiting. Per workload serious, la loro rete edge distribuita elimina la necessità di gestire un cluster Redis dedicato solo per questo scopo.
Layer 2: token budgeting applicativo per utente
L'edge ha filtrato abusi grezzi. Ora devi tracciare il costo reale per ogni utente autenticato, misurato in token consumati, non in request count. L'implementazione che uso su un progetto Laravel interno lavora con Redis come storage, sliding window di 24h, con tre contatori per ogni utente: tokens_input_24h, tokens_output_24h, tokens_total_monthly. Ogni chiamata all'LLM conta i token effettivamente consumati (li ottieni nella risposta dell'API Anthropic via usage.input_tokens e usage.output_tokens) e li scrive nei contatori Redis con TTL appropriato.
final class TokenBudgetMiddleware
{
public function __construct(
private readonly Redis $redis,
private readonly TokenBudgetRepository $budgets,
private readonly AlertService $alerts,
) {}
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
if (!$user) {
return response()->json(['error' => 'unauthenticated'], 401);
}
$budget = $this->budgets->forUser($user);
$consumed = (int) $this->redis->get("tok:month:{$user->id}") ?? 0;
if ($consumed >= $budget->monthlyHardCap) {
return response()->json([
'error' => 'monthly_budget_exceeded',
'reset_at' => $budget->resetAt()->toIso8601String(),
'consumed_tokens' => $consumed,
'cap_tokens' => $budget->monthlyHardCap,
], 429);
}
$dailyConsumed = (int) $this->redis->get("tok:day:{$user->id}") ?? 0;
if ($dailyConsumed >= $budget->dailySoftCap) {
$request->attributes->set('llm_degraded_mode', true);
}
$response = $next($request);
if ($response instanceof LlmApiResponse) {
$used = $response->tokensUsed();
$this->redis->incrby("tok:day:{$user->id}", $used);
$this->redis->incrby("tok:month:{$user->id}", $used);
$this->redis->expire("tok:day:{$user->id}", 86400);
$this->redis->expire("tok:month:{$user->id}", 86400 * 31);
if ($consumed + $used > $budget->monthlyHardCap * 0.8) {
$this->alerts->notify('budget_80_threshold', [
'user_id' => $user->id,
'consumed' => $consumed + $used,
'cap' => $budget->monthlyHardCap,
]);
}
}
return $response;
}
}Due elementi meritano attenzione. Il primo è il dailySoftCap: quando l'utente supera il budget giornaliero soft (es. 80% del cap mensile diviso 30 giorni), non viene bloccato ma viene marcato come degraded_mode. L'applicazione legge questo flag e può instradare la richiesta su un modello più economico (da Claude Sonnet a Claude Haiku, o da managed API a self-hosted), oppure ridurre la verbosità del prompt. Il servizio continua a funzionare, il costo unitario scende, l'utente riceve una notifica dashboard ("stai superando il tuo budget giornaliero, le risposte saranno semplificate") ma non perde funzionalità. Il secondo è l'alert sugli 80% del cap mensile: arriva 4-5 giorni prima del blocco potenziale e dà tempo di intervenire - alzare il cap per utenti power legittimi, verificare se è un attacco, contattare l'utente.
Layer 3: circuit breaker globale e alert anomalie
Oltre ai contatori per utente esiste un contatore globale - tutti gli utenti sommati - che fa da backstop di ultima istanza. Se il consumo aggregato cresce in modo anomalo rispetto al baseline statistico, qualcosa sta andando storto: o un bug nel codice applicativo che manda in loop le chiamate, o un attacco coordinato, o un nuovo feature release che ha sottostimato il volume. Il circuit breaker globale è configurato con soglie assolute e soglie relative (delta rispetto alla media mobile dei 7 giorni precedenti) e può, in caso di allerta rossa, sospendere tutti i nuovi workflow AI mantenendo attivi solo quelli già iniziati.
Metriche che monitoro su Prometheus con alert Grafana → Telegram:
- Token consumati per minuto, aggregato. Soglia: 150% della media mobile 7d. Trigger: warning.
- Token consumati per utente per minuto, top 10 percentile. Soglia: 300% del 95° percentile. Trigger: warning per l'utente specifico.
- Costo stimato dell'ora corrente, aggregato. Soglia: 10× il costo medio orario previsto dal budget mensile. Trigger: critical, circuit breaker globale attivato.
- Rapporto cache miss / cache hit per endpoint. Soglia: drop del 30% del hit rate rispetto alla media 7d. Trigger: warning (sintomo di input dinamici anomali).
Il costo stimato dell'ora corrente viene calcolato moltiplicando i token osservati per il pricing per modello. Anthropic pubblica il pricing ufficiale sui propri modelli Claude e il dato è aggiornato in tempo reale nel mio sistema via il response dell'API che include usage completo per ogni chiamata. Il circuit breaker scatta scrivendo una chiave Redis circuit:llm:global = open che il middleware di Layer 2 legge prima di ogni chiamata e, se trovata, rigetta immediatamente con 503 Service Unavailable.
Layer 4: audit trail e cache semantica
L'ultimo layer è meno visibile ma fondamentale per la governance a medio termine. Ogni chiamata LLM viene registrata in un log JSON Lines append-only con: timestamp, user_id anonimizzato, endpoint invocato, hash SHA-256 del prompt, hash SHA-256 della risposta, token consumati, modello usato, costo stimato, latenza. Il log serve a due scopi: audit post-incident se mai serve ricostruire un anomaly, e analisi di ottimizzazione (quali sono i prompt più frequenti? Qual è il costo medio per utente? Dove conviene introdurre caching?).
La cache semantica è il layer di ottimizzazione più sottovalutato. Non è la cache tradizionale key-value (dove la chiave è l'hash esatto del prompt): è una cache che riconosce semanticamente prompt simili tramite embeddings e restituisce la risposta cached se la similarità coseno supera una soglia (tipicamente 0.95). Implementazione: quando arriva un nuovo prompt, calcoli l'embedding con un modello piccolo (text-embedding-3-small o equivalente locale come bge-m3), cerchi in un index vettoriale pgvector i top-5 prompt simili, se il miglior match supera la soglia e il risultato è ancora fresco (TTL 24h), restituisci il cached. Sui workload reali della mia sandbox la cache semantica riduce le chiamate effettive all'LLM del 22-38% a seconda del dominio, con impatto diretto sul costo totale.
Come misurare se il tuo cost governance funziona davvero
Non basta implementare i quattro layer: serve validare che funzionino sotto stress. Tre test operativi che eseguo periodicamente, che consiglio a chiunque stia mettendo in produzione applicazioni AI in PMI italiane dove, secondo l'Osservatorio Artificial Intelligence del PoliMI presentato il 5 febbraio 2026, solo il 9% delle grandi imprese ha governance strutturata con budget monitorato:
Il primo test è un load test controllato: lancio un client che invia richieste legittime a burst crescente (1, 5, 20, 100 req/min) e verifico che gli alert scattino alle soglie corrette, che il circuit breaker si attivi alla soglia critical, che gli utenti diversi non si impattino a vicenda (isolamento). Il secondo test è la simulazione di denial-of-wallet: stesso client ma con payload artificiosamente gonfi (prompt con 80.000 token ciascuno) che simulano un attacco costoso a basso volume. Verifico che il layer 2 rilevi il consumo anomalo in token anche se il volume richieste è normale. Il terzo test è il chaos engineering della cache semantica: disabilito temporaneamente la cache e misuro l'impatto sul costo aggregato - questo mi dà il valore economico reale della cache e giustifica l'investimento infrastrutturale.
Non ti ho parlato di modelli frontier vs modelli self-hosted in questo articolo, perché è una discussione ortogonale: sia che tu paghi Claude via API, sia che tu giri Llama self-hosted su VPS Hetzner, il cost governance ti serve comunque. Nel primo caso protegge la fattura; nel secondo caso protegge la GPU da thermal throttling e la disponibilità del servizio. I layer sono gli stessi, le soglie assolute cambiano.
Se gestisci o stai progettando un'applicazione AI in una PMI con budget dichiarato e vuoi capire se l'architettura di cost governance che ho descritto si adatta al tuo caso, il modulo di preventivo gratuito ti dà una prima lettura in 7 domande, 2 minuti: ti dico se il tuo progetto rientra nelle cose che so fare bene, come si imposterebbe un primo confronto, quali domande aggiuntive ha senso farci. Se il caso richiede un profilo diverso dal mio, te lo dico e ti indico una direzione utile quando possibile.