Rate limiting su Cloudflare Workers: binding Rate Limiting, regole WAF o Durable Objects

Tre modi per rispondere 429 su Cloudflare. Il binding Rate Limiting conta per location, le regole WAF bloccano prima del Worker ma non vedono la tua logica, un Durable Object conta in modo esatto ma ogni controllo si paga. Il criterio è quanto deve essere preciso il limite.

Francesco Pio Nocerino6 minCloudflare
Rate limiting su Cloudflare Workers: binding Rate Limiting, regole WAF o Durable Objects
Indice

Questo pezzo è per chi ha un'API o un endpoint su Cloudflare Workers e deve iniziare a rispondere 429. Cloudflare offre tre strade: il binding Rate Limiting dentro il Worker, le regole di rate limiting del WAF davanti al Worker, e un contatore scritto a mano in un Durable Object. Sembrano alternative equivalenti. Non lo sono.

In breve: la scelta dipende da dove vive il contatore. Il binding Rate Limiting conta per location Cloudflare ed è volutamente approssimato, ma è veloce e conosce la tua logica. Le regole WAF bloccano prima che il Worker parta, però vedono solo la richiesta e sul piano Free contano solo per IP. Un Durable Object dà un conteggio esatto e globale per chiave, ma ogni controllo è una richiesta in più da pagare. Per la maggior parte delle API basta il binding; il WAF serve per gli abusi grossolani; il Durable Object quando il limite è un contratto.

La domanda giusta

Non "quale rate limiter è il migliore?". È: quanto deve essere preciso il limite, e chi lo deve sapere?

Un limite anti-abuso su /login può sbagliare di qualche richiesta. Una quota di 1.000 chiamate al mese venduta a un cliente no. Le tre opzioni si dividono proprio su questo.

Il binding Rate Limiting: dentro il Worker, per location

Il binding Rate Limiting è disponibile in GA dal 19 settembre 2025. Lo dichiari nella configurazione del Worker e lo chiami dal codice con una chiave scelta da te.

{
  "ratelimits": [
    {
      "name": "API_LIMITER",
      "namespace_id": "1001",
      "simple": { "limit": 100, "period": 60 }
    }
  ]
}
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const apiKey = request.headers.get("Authorization") ?? "anonimo";
    const { success } = await env.API_LIMITER.limit({ key: apiKey });
    if (!success) {
      return new Response("Troppe richieste", { status: 429 });
    }
    return new Response("ok");
  },
} satisfies ExportedHandler<Env>;

Quattro vincoli dalla documentazione, da sapere prima di usarlo:

  • Finestre fisse. period può valere solo 10 o 60 secondi. Un limite orario o giornaliero non si esprime così.
  • Per location. Il limite vale per ogni location Cloudflare. Con 100 richieste al minuto, un client servito da due location diverse può farne di più.
  • Permissivo. I contatori sono in cache sulla macchina del Worker e si aggiornano in background. La documentazione lo definisce eventualmente consistente e "non pensato come sistema di contabilità".
  • Namespace condiviso. Due binding con lo stesso namespace_id, anche in Worker diversi dello stesso account, condividono i contatori. Utile se lo vuoi, una sorpresa se no.

Il vantaggio è la latenza: await limit() non fa una richiesta di rete. E la chiave la scegli tu, quindi puoi limitare per utente, per tenant o per piano. La documentazione sconsiglia l'IP come chiave, perché molti utenti legittimi condividono lo stesso indirizzo.

Un dettaglio operativo: oggi questi limiti non compaiono nella dashboard. Per vedere quanti 429 restituisci servono i log del Worker o un evento su Analytics Engine.

Le regole WAF: prima del Worker, per data center

Le regole di rate limiting del WAF stanno sulla zona, cioè sul dominio gestito da Cloudflare. Agiscono prima che la richiesta arrivi al tuo codice. Il binding, dice la documentazione, usa la stessa infrastruttura.

Il vantaggio è netto: una richiesta bloccata dal WAF non esegue il Worker. Per un attacco a forza bruta su un endpoint di login è il posto giusto.

Il limite è che il WAF vede solo la richiesta, non la tua logica. Non sa se l'utente è sul piano gratuito o pagante, a meno che l'informazione non sia in un header o nel path. E quello che può vedere dipende dal piano:

Piano

Campi nell'espressione

Conteggio per

Finestre

Regole

Free

Path, Verified Bot

IP

10 s

1

Pro

Host, URI, Path, Query e altri

IP

fino a 1 min

2

Business

anche Method, IP, User Agent

IP, IP con NAT

fino a 10 min

5

Enterprise

header della richiesta; corpo solo con Advanced Rate Limiting

IP, IP con NAT; header, cookie, JSON solo con Advanced Rate Limiting

fino a 65.535 s

100 (da contratto)

Sul piano Free hai quindi una sola regola, che conta per IP su finestre di 10 secondi. Basta per proteggere un path ovvio. Non basta per una quota per API key.

Anche qui la precisione non è garantita. La pagina sul calcolo del rate spiega che ogni data center ha i suoi contatori e che non esiste un contatore globale. La documentazione avverte che, per un ritardo di qualche secondo nell'aggiornamento, alcune richieste in eccesso possono passare. Il codice di risposta predefinito per il blocco è 429.

Il Durable Object: esatto e globale, ma lo paghi

Un Durable Object è un'istanza unica al mondo per un dato nome, a thread singolo, con storage fortemente consistente. Se crei un oggetto per ogni chiave da limitare, tutte le richieste di quella chiave passano dallo stesso contatore. Il conteggio è esatto e non dipende dalla location.

Un contatore a finestra fissa, nella sua forma minima:

import { DurableObject } from "cloudflare:workers";

export class RateLimiter extends DurableObject {
  async hit(limit: number, windowMs: number): Promise<boolean> {
    const now = Date.now();
    const start = now - (now % windowMs);
    const saved = await this.ctx.storage.get<{ start: number; count: number }>("w");
    const w = saved && saved.start === start ? saved : { start, count: 0 };
    if (w.count >= limit) return false;
    w.count++;
    await this.ctx.storage.put("w", w);
    return true;
  }
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const apiKey = request.headers.get("Authorization") ?? "anonimo";
    const stub = env.RATE_LIMITER.get(env.RATE_LIMITER.idFromName(apiKey));
    if (!(await stub.hit(1000, 3_600_000))) {
      return new Response("Quota oraria esaurita", { status: 429 });
    }
    return new Response("ok");
  },
} satisfies ExportedHandler<Env>;

Qui la finestra la decidi tu: un'ora, un giorno, un mese. E puoi cambiare algoritmo, per esempio una finestra scorrevole o un token bucket, senza dipendere da quello che offre la piattaforma.

Il prezzo è in tre voci. Ogni controllo è una chiamata di rete verso l'oggetto, che può trovarsi lontano dall'utente. Ogni oggetto ha un limite indicativo di 1.000 richieste al secondo, quindi una sola chiave molto calda diventa un collo di bottiglia. E ogni controllo si paga.

Dalla pagina prezzi dei Durable Objects, sul piano Workers Paid: 1 milione di richieste al mese incluse, poi 0,15 dollari per milione. Le righe scritte sono incluse fino a 50 milioni al mese, poi 1 dollaro per milione. In più c'è la durata di esecuzione e il minimo di 5 dollari al mese del piano. In un esempio ipotetico con 10 milioni di controlli al mese, le sole richieste costano circa 1,35 dollari e le scritture restano nell'incluso. Pochi soldi, ma è una voce che il binding non ha: la pagina prezzi di Workers non elenca un costo separato per il binding Rate Limiting.

Il confronto sul vincolo

Criterio

Binding Rate Limiting

Regole WAF

Durable Object

Dove agisce

dentro il Worker

prima del Worker

dentro il Worker, via RPC

Ambito del contatore

per location

per data center

globale per chiave

Precisione

approssimata

approssimata

esatta

Finestre

10 o 60 s

da 10 s a 65.535 s secondo il piano

quelle che scrivi

Chiave

qualsiasi stringa

IP (Free e Pro), di più sui piani alti

qualsiasi stringa

Latenza aggiunta

trascurabile

nessuna sul Worker

una chiamata di rete

Costo

nessuna voce separata in listino

incluso nel piano, una regola sul Free

richieste, righe e durata DO

Codice da mantenere

poche righe

nessuno

il contatore è tuo

Quale scegliere

Usa il binding quando vuoi limitare per utente, API key o route e ti va bene un margine. È il caso della maggior parte delle API pubbliche, delle form e degli endpoint che chiamano un servizio esterno costoso.

Usa il WAF quando vuoi fermare traffico ovviamente abusivo prima che costi esecuzioni del Worker. Login, endpoint di registrazione, path che nessun client legittimo chiama dieci volte in dieci secondi. Funziona anche se il Worker non c'è.

Usa un Durable Object quando il limite è parte del prodotto: quote mensili, piani a consumo, crediti. Lì un margine "eventualmente consistente" significa regalare o negare qualcosa che hai venduto.

Non sono esclusivi. Una combinazione sensata è il WAF davanti al login, il binding su tutte le route API e un Durable Object solo per la quota che finisce in fattura.

Se il limite che ti interessa è sulle chiamate ai modelli, guarda prima il rate limiting di AI Gateway: si configura sul gateway, con finestra fissa o scorrevole, e affianca gli spend limit in dollari.

Cosa conviene fare adesso

  • Scrivi in una riga cosa succede se il limite sbaglia di qualche richiesta. Se la risposta è "niente di grave", parti dal binding.
  • Scegli la chiave prima dello strumento: API key, ID utente o tenant. L'IP solo come ultima risorsa.
  • Se sei sul piano Free, usa l'unica regola WAF per il path più attaccato, di solito il login.
  • Usa un namespace_id diverso per ogni limite, a meno che tu non voglia davvero condividere i contatori tra Worker.
  • Registra ogni 429 nei log o su Analytics Engine: la dashboard oggi non li mostra.
  • Tieni il Durable Object per le quote che vendi, e stima il costo sulle chiamate reali prima di metterlo su ogni richiesta.

Se stai ancora decidendo dove ospitare l'API, il confronto Vercel vs Cloudflare vs VPS mette questi vincoli accanto agli altri. Per lo stack completo su Cloudflare c'è il pezzo su Workers, D1 e R2, e per il deploy quello su cf al posto di Wrangler.

[ DOMANDE FREQUENTI ]

Domande frequenti

01Il binding Rate Limiting di Workers è un limite globale?

No. La documentazione dice che il limite vale per ogni location Cloudflare: per ogni chiave c'è un contatore separato in ogni location. Ed è volutamente permissivo, quindi qualche richiesta oltre il limite può passare.

02Le regole di rate limiting del WAF sono disponibili sul piano Free?

Sì, con vincoli stretti: una regola, conteggio per IP, finestra e blocco di 10 secondi, e nell'espressione solo il path e i bot verificati. I piani Pro e Business alzano numero di regole, finestre e campi disponibili.

03Quando serve davvero un Durable Object per il rate limiting?

Quando il conteggio deve essere esatto e uguale ovunque, per esempio una quota mensile legata a un piano a pagamento. Per proteggere un'API dagli abusi il binding o il WAF bastano e costano meno.

Condividi