bext.dev
DocsApplication Toolkit
Application Toolkit2 min read
On this page

Rate Limiting

@bext-stack/framework/rate-limit throttles requests the Laravel RateLimiter way: a fixed window per key, with check / attempt / peek / reset and a result carrying allowed, remaining, and retryAfterSecs.

When To Use It#

  • Protect a login, signup, or password-reset action from brute force.
  • Cap an expensive or abusable endpoint per client / per user.
  • Any "N per window" rule.

Guarding an action#

ts
import { createRateLimiter, memoryRateStore, keyForRequest } from "@bext-stack/framework/rate-limit";

const limiter = createRateLimiter({ store: memoryRateStore() });

export async function action({ request }) {
  const key = keyForRequest(request, "login");            // client IP + bucket
  const r = await limiter.check(key, { max: 5, windowSecs: 60 });
  if (!r.allowed) {
    return new Response("Too many requests", {
      status: 429,
      headers: { "retry-after": String(r.retryAfterSecs) },
    });
  }
  // …authenticate…
  if (loginSucceeded) await limiter.reset(key);           // clear on success
}

check registers a hit and reports the verdict. attempt(key, opts, fn) runs fn only when under the limit and returns the result alongside the state:

ts
const out = await limiter.attempt(key, { max: 5, windowSecs: 60 }, () => sendCode());
if (!out.allowed) { /* throttled — out.retryAfterSecs */ }
Member Does
check(key, { max, windowSecs }) register a hit → RateLimitResult
peek(key, opts) the state without consuming a hit
attempt(key, opts, fn) run fn only if allowed; result carries the outcome
reset(key) clear a key's window
keyForRequest(request, bucket?) "<bucket>:<client-ip>" from proxy headers

RateLimitResult: { allowed, limit, remaining, retryAfterSecs, resetAt }.

Stores#

The limiter is a thin layer over a RateLimitStore:

  • memoryRateStore() — in-process fixed window. Per-V8-isolate — fine for a single worker and tests.
  • Bring your own (implement hit/peek/reset) backed by a shared store to limit across a worker fleet.
Tip

Use keyForRequest(request, "signup") so different actions throttle independently, and prefer a shared store in production — a per-isolate memory store limits each worker separately, so the effective limit is max × workers.

Try It#

The live rate-limiting demo fires a burst of 6 requests at a limit of 3 — the first three pass, the rest are blocked with a retry-after. Source in sites/demo/src/app/examples/rate-limit/page.tsx.

See Also#

Edit this page ↗Need a hand? ↗
FIND YOUR NEXT STEP

Start with a topic, a command, or a question.