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#
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:
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.
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#
- Security — other request-hardening measures.
- Authentication — the login flow to protect.
- Application Toolkit — the rest of the TypeScript app layer.