🛡️Gatekeeper/ SDKs

Gatekeeper throttles abusive traffic with fixed-window rate limits. The limit fronts the rest of the decision pipeline - it runs before identity resolution, so brute force on a credential is throttled before any expensive backend call. When a window is exceeded the request gets a 429 with a Retry-After header.

The tiers#

There are four independent tiers, each a (limit, period) fixed window. The period is in seconds and is constrained to 10 or 60 (a platform constraint of the edge limiter). These are the defaults; each is overridable per deploy.

TierDefault limitWindowGuards
credential560sThe user-credential path: signup, login, refresh - the brute-force surface.
key6060sAPI-key validate / token operations.
platformCredential6060sPlatform service-account and bootstrap credential operations.
webhook60060sThe inbound payment-webhook endpoint.

The tightest budget is credential on purpose: a handful of attempts per minute per source is plenty for a real user and starves a password-guessing loop.

What a throttled response looks like#

When a tier is exceeded:

// 429 response body
{ "ok": false, "error": "too many requests", "code": "rate_limited" }
// header: Retry-After: 60

If the limiter itself or the forge-proof client IP is unavailable, the request fails closed with 503 (rate limiting unavailable) rather than being let through.

Edge-atomic vs in-memory#

How the window is counted depends on the runtime:

RuntimeMechanismProperty
Cloudflare WorkersNative [[ratelimits]] edge bindingThe increment is atomic per key, so a concurrent burst cannot overshoot the limit.
BunA cache-backed fixed-window limiter behind the same minimal contractA read-then-write counter; under heavy concurrency a burst can slightly overshoot before the window settles.

Both satisfy the same minimal limiter contract (limit({ key }) => { success }), which is why the identical engine runs on both. The atomic edge path is the production posture; the in-memory path is a faithful local-development equivalent with the overshoot caveat above.

How the SDK surfaces a 429#

A 429 becomes a GatekeeperError with code === 'rate_limited' and a populated retryAfter (parsed from the Retry-After header). Back off for that many seconds before retrying.

import { GatekeeperError } from '@orkait/sdk';
 
async function withBackoff<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
  for (let i = 0; ; i++) {
    try {
      return await fn();
    } catch (err) {
      if (err instanceof GatekeeperError && err.code === 'rate_limited' && i < attempts) {
        const waitMs = (err.retryAfter ?? 1) * 1000;
        await new Promise((r) => setTimeout(r, waitMs));
        continue;
      }
      throw err;
    }
  }
}