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. Every limit counted today is a
minute's (600 for a signed-in caller, 120 for an address), so all of
them map onto the binding. The error for any other window comes with a
route's own
rate_limit:, the first thing that can write one. Retry-Afteris the window's whole length, since the binding says only whether a request is within the limit, not when its window ends.
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 a707efb.