Built-in servicesRate limitsDetails
Rate limits on Workers
On this page
A Worker's instances share no memory, so on Cloudflare Workers your rate limits need a store that every instance can reach. This topic is how that store works (Rate limits has the limits themselves).
Choosing the store
rate_limits: { store: } in application { } names one connection,
with Driver.redis or Driver.cloudflare_rate_limits, and nothing else
(rate_limits.store_invalid).
A deployable built for Workers that serves an API needs a store
(rate_limits.store_required). One that serves no API has no routes to
limit, and counts the requests it refuses beside its health checks in
its own memory.
Cloudflare's binding: Driver.cloudflare_rate_limits
Driver.cloudflare_rate_limits runs only on Workers, so a native
deployable that serves an API with that store is refused
(deployable.target_unsupported), naming Redis. Its namespace: is a
whole number of at least 1.
- One binding for each limit. A binding counts one limit over one
window, so each limit your app counts is a binding of its own. It is
named for the connection and the limit, such as
LIMITS_120_PER_60S, and numbered fromnamespace:in order of window and then of requests.jadpo buildwrites each into the Wrangler file asratelimits: [{ name, namespace_id, simple: { limit, period } }]. Adding a limit can renumber the ones after it, which loses at most a window's counts, and those are harmless to lose. - Windows of 10 and 60 seconds. The default limits are a minute's
(600 for a signed-in caller, 120 for an address). A route or an API that
writes any other window is refused where it wrote it
(
rate_limits.window_unsupported). - A deployable's bindings. Each is numbered from every limit the app writes, so a limit has the same namespace in every deployable, and a deployable's Wrangler file names only those its own routes count.
Retry-Afteris the window's whole length, since the binding says only whether a request is within the limit, not when its window ends.
What the binding promises
Treat a limit in this binding as rough: it refuses a client that keeps on asking, but well past the number you wrote. Here is what it did, measured once, from one location (London, LHR), on 2026-11-04, with one client and no other traffic on the account:
| Limit | What happened |
|---|---|
| 5 in 10 seconds, one request after another | 24 passed before the first 429 |
| 10 in 60 seconds, one after another | 49 passed before the first 429 |
| 120 in 60 seconds, one after another | 200 passed with none refused, when the check stopped |
| 5 in 10 seconds, 60 sent at once | 44 passed, 16 refused |
Once its window had passed, the 5-in-10-seconds route answered again. Each
429 gave the window's length in Retry-After. Those are one run's numbers,
not a promise: they show how far past a limit a client can get, and it may
differ elsewhere and under other traffic.
Cloudflare documents the binding as approximate, and these are its other terms. A limit on Workers is only as strong as this:
- Per location. Cloudflare says each of its locations keeps its own count,
and they don't add up to one worldwide limit, so
requests: 5applies roughly in each location a client's requests reach. This hasn't been measured from a second location. - Approximate. Each check reads a count kept where the Worker runs, and counts update in the background, so requests pass the limit, as the table shows. It isn't an accounting system. Cloudflare's own answer for a strict limit is a limiter on a Durable Object, which Jadpo doesn't build.
- An address can be many people. Cloudflare warns against keying a limit
on an address, since many users can share one on mobile networks. Prefer
RateLimitKey.callerwhere callers sign in, and don't set a limit by address so low that a shared address trips it. - A namespace id is the whole account's. Cloudflare says two bindings with
one
namespace_idshare their counts for a key, across Workers, but two deployables of one app didn't in a single measurement (LHR, 2026-11-04): three times, one was refused after 87, 81 and 10 requests at 10 in 60 seconds and the other then answered three requests. Don't rely on either behaviour. Give another app in the account anamespace:far enough from this one's that its numbers don't meet them.
So use the binding to shed abuse, not to enforce a total (What a limit doesn't promise).
When the binding can't be asked
- Falling back. If the Worker hasn't got the binding, or the binding throws or rejects, the request is counted in the isolate's own memory, so no request fails for it.
- Not waited for. A binding that hasn't answered within 500 ms isn't waited for: the request is counted in the isolate's memory, as for one that throws. The binding answers from a count kept where the Worker runs, so one slower than that is in trouble, and a request isn't held up by it.
- Logged once. Each binding that can't be asked is logged once for
each isolate, as
rate_limits.fallback, with the binding's name and why (missing,failedortimed_out), never a key. An operator sees that a Wrangler file lost its bindings, without a log line for every request.
A Redis store on Workers
Until Redis's driver comes with caches, a deployable whose store is
Redis counts in its own memory, the same fallback as when the store
can't be reached. A Worker's instances share no memory, so a deployable
built for Workers that serves an API with a Redis store is
rate_limits.counted_in_memory. It is a judgement call, since Jadpo has
no warnings. You answer it with Cloudflare's binding, or with a
decline line above the deployable that keeps Redis and counts in
memory until its driver comes. A native deployable with a Redis store is
unaffected.
The windows and the Wrangler entries aren't yet checked against the real
edge: the workerd pinned in tools/fixtures/workers/ has no binding of
its own, and its types give only limit({ key }) and success
(What is built).
Markdown for agents: /docs/rate-limits-on-workers.md.Built from Jadpo main at aee2e8f.