Rate Limits
gate.rate — KV-backed throttles with max, per, and keyBy; five strategies, sliding-window-counter by default.
gate.rate protects Flows from abuse and runaway clients. Limits run as atomic takes on the KV
driver before do. Compose rates with policies via gate.all or list them on .gate(...).
For developers throttling login, writes, and expensive reads on okengine — declare the window, key the subject, attach on the trigger.
The one rule
Pass options as one object: { max, per, keyBy? }. There is no gate.rate(name, { limit, window })
form — the runtime names the gate from strategy and window.
Smallest Example
Declare a rate gate
import { gate } from "okengine";
export const loginThrottle = gate.rate({
strategy: "sliding-window-counter", // default when omitted
max: 5,
per: "1m",
keyBy: "ip",
description: "Login attempts per IP",
});The runtime name is rate:sliding-window-counter:5/1m (strategy · max · per).
Attach on the trigger
import { on, flow, http, gate } from "okengine";
import { loginThrottle } from "@/core/gate";
export const login = on(
http.post("/auth/sign-in/email").gate(gate.public, loginThrottle),
flow({
do: async (input, fx) => {
// At most 5 takes per IP per minute
return input;
},
}),
);See a burned quota
{
"data": null,
"error": {
"code": "RateLimited",
"message": "Too many requests. Try again later.",
"data": { "retryAfterMs": 42000 }
}
}Status is 429 Too Many Requests. Clients should wait retryAfterMs before retrying.
Progressive Patterns
From the default counter to bursty buckets and subject keys:
Omit strategy for sliding-window-counter — best accuracy-to-cost ratio (two KV keys, no
boundary bursts):
export const fair = gate.rate({ max: 60, per: "1m", keyBy: "user" });Strategies
| Strategy | Best for | Cost shape |
|---|---|---|
sliding-window-counter | Default — accuracy vs cost | Two keys, weighted estimate |
token-bucket | Bursty traffic with refill | One hash (tokens, ts) |
leaky-bucket | Smooth outbound rate | One hash (level, ts) |
fixed-window | Simple reset intervals | One counter per bucket |
sliding-log | Strict precision | ZSET of event timestamps |
All five run as atomic Lua on the KV driver (memory / redis EVAL).
Detailed section
Pick the strategy from product physics, not fashion. The default covers most HTTP write paths; switch only when you need bursts, smooth leak, or exact event logs.
Near-exact rolling window without boundary spikes of a naive fixed window:
gate.rate({
strategy: "sliding-window-counter", // or omit — this is the default
max: 100,
per: "1m",
keyBy: "user",
});Uses current + previous window counters with a time-weighted estimate.
Options
| Option | Type | Default | Meaning |
|---|---|---|---|
max | number | required | Takes allowed within per (> 0) |
per | string | required | Window / refill ("1m", "60s", "1h", …) |
strategy | RateStrategy | sliding-window-counter | Algorithm id |
keyBy | string | global | Subject dim ("ip", "user", …) |
overridable | boolean | false | Console may override max / per in Store |
description | string | — | Human label for Console / docs |
Consequence: rate gates need a KV driver in the app. Without KV, evaluation denies with
telemetry reason "rate gate requires kv" and the typed code is still RateLimited.
Declare-time guards:
gate.rate: max must be a positive numbergate.rate: per is required
Invalid per at evaluation → deny with reason: "invalid per: …".
Naming & Overrides
The runtime name is always rate:{strategy}:{max}/{per}:
rate:sliding-window-counter:60/1m
rate:token-bucket:20/1mWhen overridable: true, Console may tune max / per in Store without a code deploy. Leave
it false (default) for hard product limits.
oke({ gate: { rateLimit: { enabled } } }) controls whether auth path Flows attach
stricter presets. Default is true when gate.auth is on, false otherwise — it does not
replace your own gate.rate declarations.
Troubleshooting
Quota burned for that subject. Inspect retryAfterMs, widen per, raise max, or change keyBy
so clients do not share one global bucket unintentionally.
Configure a KV driver (memory for local, redis for multi-instance). Policy-only apps can omit
KV; any gate.rate on a hot path cannot. The HTTP envelope is still RateLimited.
With keyBy: "user", missing userId maps to "anon" — shared. Put a member policy before the
rate, or key by "ip" for public endpoints.
max must be > 0. Zero and negatives fail at declare.
Pass a duration string ("1m", "30s", …). Empty / missing per fails at declare.
The duration string did not parse to a positive window. Use ms|s|m|h|d style values the Clock
duration parser accepts ("1m", "60s", "1h").
Learn more
- Gate Overview — chain order and denial mapping
- Authorization — policies on the same
.gate(...) - Store · KV — KV facet drivers
- HTTP — attach rates on REST routes