ElementsGate

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

src/core/gate.ts
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

src/flows/auth/login.ts
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

StrategyBest forCost shape
sliding-window-counterDefault — accuracy vs costTwo keys, weighted estimate
token-bucketBursty traffic with refillOne hash (tokens, ts)
leaky-bucketSmooth outbound rateOne hash (level, ts)
fixed-windowSimple reset intervalsOne counter per bucket
sliding-logStrict precisionZSET 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

OptionTypeDefaultMeaning
maxnumberrequiredTakes allowed within per (> 0)
perstringrequiredWindow / refill ("1m", "60s", "1h", …)
strategyRateStrategysliding-window-counterAlgorithm id
keyBystringglobalSubject dim ("ip", "user", …)
overridablebooleanfalseConsole may override max / per in Store
descriptionstringHuman 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 number
  • gate.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/1m

When 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

Learn more

Next

On this page