The Rate Limiting binding counts per key inside the Worker

Declare ratelimits with a limit and a 10 or 60 s period, then await env.LIMITER.limit({ key }). It answers success: false in microseconds, before any work.

Security

· Chapter

42

·

2

min read

The answer. Rate limiting used to mean a KV counter and a race condition. The Rate Limiting binding is a purpose-built counter that lives in the runtime: you declare a name, a limit and a period (10 or 60 seconds) in config, then call env.LIMITER.limit({ key }) with whatever identifies the caller: an IP, an email, a token, a tenant id. The call returns { success } without a network round trip. Our guide-delivery Worker keeps two of them, one on form submissions per IP and a stricter one on outbound emails per address, and checks both before Turnstile verification and before anything reaches the queue. Counters are approximate and local to the Cloudflare location handling the request, which is exactly right for abuse control and wrong for billing quotas.

The pattern.

{ "ratelimits": [
  { "name": "SUBMISSION_LIMITER", "namespace_id": "1001", "simple": { "limit": 5, "period": 60 } },
  { "name": "EMAIL_LIMITER",      "namespace_id": "1002", "simple": { "limit": 3, "period": 60 } } ] }
const ip = request.headers.get("CF-Connecting-IP") ?? "unknown";
const { success } = await env.SUBMISSION_LIMITER.limit({ key: ip });
if (!success) return new Response("slow down", { status: 429, headers: { "retry-after": "60" } });

Watch out.

  • namespace_id is an integer you choose, unique per Worker. Two bindings with the same id share one counter.
  • Per-location counting means a distributed attacker gets limit per location, not limit globally. Pair it with WAF rate-limiting rules on the zone for the global view.
  • The key is the whole design. Rate-limit by the resource you are protecting (an email address for sends), not only by IP.

Related: turnstile-is-only-a-widget-until-siteverify · kill-switch-var-fail-closed-ceilings-fail-open · queues-batches-acks-and-retries