Runtime APIs
Rate Limiting
Use the Workers Rate Limiting binding with OpenOrange, backed by a local Rivet Actor.
Status: Supported. Runs on one Rivet Actor for each namespace.
Configuration
OpenOrange reads the standard Wrangler ratelimits binding. No extra setup is
needed:
{
"ratelimits": [
{
"name": "MY_RATE_LIMITER",
"namespace_id": "1001",
"simple": { "limit": 100, "period": 60 }
}
]
}
The older unsafe.bindings form with "type": "ratelimit" and the same
fields also works.
Like wrangler dev, orange dev stops at startup when simple.period is not
10 or 60, or when simple.limit is not a number. The error names the binding.
Usage
export default {
async fetch(request, env) {
const key = request.headers.get("cf-connecting-ip") ?? "anonymous";
const { success } = await env.MY_RATE_LIMITER.limit({ key });
if (!success) return new Response("Too Many Requests", { status: 429 });
return new Response("ok");
},
};
How it works
Each namespace_id is one internal Durable Object, which runs as a Rivet Actor
named openorange:ratelimit with SQLite storage. The counters persist across
orange dev restarts in the project’s .open-orange directory.
The counter is a fixed window, like wrangler dev:
- The window is
floor(now / period), aligned to the wall clock. - Each
(key, period)pair has one counter. Bindings with the samenamespace_idshare counters. - A call increments the counter and returns
{ success: true }until the counter reacheslimit. Then it returns{ success: false }until the next window. - Each call reads and updates the counter in one SQLite transaction, so
overlapping calls never allow more than
limit.
limit() checks its options like wrangler dev. For example, an unknown
option rejects with bad rate limit options: [window]. Like wrangler dev,
a call can also override limit and period.
Conformance
An end-to-end orange dev test covers the limit per key, separate keys and
namespaces, shared namespaces, the next window, overlapping calls on one key,
option errors, both configuration forms, and persistence across restarts.
Limits and deviations
- Cloudflare counts per location and is eventually consistent, so it can allow
a few more calls than
limit. OpenOrange has one exact counter pernamespace_id. It never allows more thanlimitin a window. - Each
limit()call is one round trip to a Rivet Actor. Cloudflare answers from a local cache.
Compatibility
From the latest conformance run. Scores on the compatibility page; every binding on the status page.
Rate Limiting
ratelimitsunsafe.bindings Runs on Internal Rivet Actor per namespace_id with an exact fixed-window counter.
Counts are exact per window, while Cloudflare counts per location. Each limit() call is one actor round trip.
Scales by Namespace. One actor per namespace_id, which holds every key's counter.
Recorded results
WorksRate LimitingLocal to each edge location, and approximate.One exact counter per namespace. 1/1 pass
- Results
- example: rate-limiting Pass
In the feature matrix
Alternativeratelimits (ratelimit)Applies a rate limit with the Workers Rate Limiting API.Each namespace_id is an Orange-internal SQLite Durable Object on Rivet Actors with a fixed-window counter, like Miniflare. Counts persist under .open-orange/rivetkit across orange dev restarts. 5 files
- Notes
- Accepts ratelimits and unsafe.bindings with type ratelimit. Config load rejects a period other than 10 or 60 and a limit that is not a number, like wrangler dev. Cloudflare counts per location and can allow more than the limit; Orange keeps one exact counter per namespace_id. Each limit() call is one actor round trip.
- Source
- src/config/load-wrangler.ts:679-704packages/runtime-node/src/runner.ts:592-600packages/runtime-node/src/bindings/ratelimit/object.ts:3-9packages/runtime-node/src/bindings/ratelimit/binding.ts:4-7src/runtime/bindings.test.ts:288-400