Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Built-in services

Built-in services

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 from namespace: in order of window and then of requests. jadpo build writes each into the Wrangler file as ratelimits: [{ 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-After is 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, failed or timed_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.