-
Notifications
You must be signed in to change notification settings - Fork 0
Frameworks and the Edge
Every adapter shares the same options (strategy/limiter, store, key, cost, fail, emit, onLimited, handler, trusted-proxy config) and the same standards headers — only the binding differs, and each is its own subpath. Beyond Express/fetch/Hono/Fastify/Koa/Next, ThrottleKit ships NestJS, SvelteKit, Remix, Elysia, AWS Lambda, tRPC, and gRPC bindings, plus a transport-agnostic createEnforcer() for anything else — all dependency-free.
import { expressRateLimit } from "throttlekit/express";
import { gcra } from "throttlekit";
app.use(
expressRateLimit({
strategy: gcra({ limit: 100, periodMs: 60_000, burst: 20 }),
// Default key is a proxy-correct, IPv6-aggregated client IP (see Operations).
key: (req) => req.headers["x-api-key"]?.toString() ?? req.ip ?? "anon",
cost: (req) => (req.method === "POST" ? 5 : 1),
fail: "open", // allow if the store is unreachable ("open" | "closed")
emit: { draft: true }, // emit IETF draft RateLimit headers (the default)
onLimited: (req, _res, d) => console.warn("blocked", req.path, d.retryAfterMs),
}),
);On a denial the middleware responds 429 with Retry-After. Pass handler to own the 429, or limiter instead of strategy to share a prebuilt limiter. See examples/express.ts.
Runs on Cloudflare Workers, Deno, Bun, and Next.js edge. The default key tries cf-connecting-ip, then x-forwarded-for (via the trusted-proxy policy), then "anon".
import { withRateLimit } from "throttlekit/fetch";
import { gcra } from "throttlekit";
export default {
fetch: withRateLimit(
(req: Request) => new Response(`hello ${new URL(req.url).pathname}`),
{ strategy: gcra({ limit: 30, periodMs: 10_000 }), emit: { draft: true } },
),
};On allow it forwards to your handler and copies the rate-limit headers onto the response; on deny it returns 429. See examples/fetch-edge.ts.
import { honoRateLimit } from "throttlekit/hono"; // edge-first
app.use("*", honoRateLimit({ strategy: gcra({ limit: 30, periodMs: 10_000 }) }));
import { fastifyRateLimit } from "throttlekit/fastify"; // Fastify v5
fastify.addHook("onRequest", fastifyRateLimit({ strategy: gcra({ limit: 100, periodMs: 60_000 }) }));
import { koaRateLimit } from "throttlekit/koa"; // Koa v3
app.use(koaRateLimit({ strategy: gcra({ limit: 100, periodMs: 60_000 }) }));Hono, Fastify, and Koa are optional peer dependencies.
Next.js middleware is dependency-free (NextRequest/NextResponse are Web Request/Response) — call the limiter, then branch:
// middleware.ts
import { NextResponse, type NextRequest } from "next/server";
import { nextRateLimit } from "throttlekit/next";
import { gcra } from "throttlekit";
const limit = nextRateLimit({ strategy: gcra({ limit: 30, periodMs: 10_000 }) });
export async function middleware(req: NextRequest) {
const r = await limit(req);
if (r.limited) return r.response; // 429 (or 503 on a fail-closed outage)
const res = NextResponse.next();
for (const [k, v] of Object.entries(r.headers)) res.headers.set(k, v);
return res;
}For Next.js route handlers, use throttlekit/fetch directly — they're Web fetch handlers.
A CanActivate guard for @UseGuards(...), keyed on the proxy-correct client IP. Dependency-free — pass exceptionFactory to throw a real HttpException for an idiomatic 429:
import { HttpException, HttpStatus } from "@nestjs/common";
import { nestRateLimit } from "throttlekit/nest";
import { gcra } from "throttlekit";
const RateLimitGuard = nestRateLimit({
strategy: gcra({ limit: 100, periodMs: 60_000 }),
exceptionFactory: (d) =>
new HttpException({ error: "Too Many Requests", retryAfterMs: d.retryAfterMs }, HttpStatus.TOO_MANY_REQUESTS),
});
// @UseGuards(RateLimitGuard) on a controller or handlerPrefer per-route config like @nestjs/throttler? Register one guard globally and annotate routes with @RateLimit({ limit, period }). Still dependency-free — it reads the ambient reflect-metadata NestJS already loads (no @nestjs/common import):
// app.module.ts — register the guard once
import { APP_GUARD } from "@nestjs/core";
import { createRateLimitGuard } from "throttlekit/nest";
import { RedisStore } from "throttlekit/redis";
@Module({
providers: [
{ provide: APP_GUARD, useValue: createRateLimitGuard({ store: new RedisStore({ client }) }) },
],
})
export class AppModule {}
// any controller — annotate a handler (or the whole class)
import { RateLimit } from "throttlekit/nest";
@Controller("posts")
export class PostsController {
@RateLimit({ limit: 100, period: "1m" }) // period: "30s" | "1m" | "1h" | "1d" | ms
@Post() create() { /* ... */ }
@RateLimit({ strategy: quota({ limit: 1_000_000, resetCadence: "calendar-month" }) })
@Post("bulk") bulk() { /* a monthly quota on one route */ }
}Routes without @RateLimit pass through (set defaults on the guard to limit them too). One limiter is built and cached per distinct config, all sharing the guard's store. A handler-level @RateLimit overrides a class-level one.
Wrap a proxy handler (REST payload v1 + HTTP API v2). Keyed by the API Gateway-provided sourceIp (platform-set, not client-spoofable); it merges the standards headers into the result and returns 429 over the limit:
import { lambdaRateLimit } from "throttlekit/lambda";
import { gcra } from "throttlekit";
import { RedisStore } from "throttlekit/redis";
export const handler = lambdaRateLimit(myHandler, {
strategy: gcra({ limit: 100, periodMs: 60_000 }),
store: new RedisStore({ client }), // a shared store — each invocation is a fresh process
});A middleware (pass it to t.middleware). You supply the key from the app-defined ctx; on a denial it throws — RateLimitExceededError by default, or a TRPCError via errorFactory:
import { TRPCError } from "@trpc/server";
import { trpcRateLimit } from "throttlekit/trpc";
const rl = trpcRateLimit<MyCtx>({
strategy: gcra({ limit: 100, periodMs: 60_000 }),
key: ({ ctx }) => ctx.user?.id ?? ctx.ip,
errorFactory: (d) => new TRPCError({ code: "TOO_MANY_REQUESTS", message: `retry in ${d.retryAfterMs}ms` }),
});
export const limited = t.procedure.use(t.middleware(rl));A unary-handler wrapper for @grpc/grpc-js. Keyed on the peer by default (override to read an API token from metadata); over the limit it completes the call with RESOURCE_EXHAUSTED:
import { grpcRateLimit } from "throttlekit/grpc";
const gate = grpcRateLimit({ strategy: gcra({ limit: 100, periodMs: 60_000 }) });
server.addService(GreeterService, { sayHello: gate.unary(sayHelloImpl) });// SvelteKit — src/hooks.server.ts (keyed on event.getClientAddress())
import { sveltekitRateLimit } from "throttlekit/sveltekit";
export const handle = sveltekitRateLimit({ strategy: gcra({ limit: 60, periodMs: 60_000 }) });
// Remix — a loader/action guard; resolves to headers, THROWS a 429 Response over the limit
import { remixRateLimit } from "throttlekit/remix";
const rateLimit = remixRateLimit({ strategy: gcra({ limit: 60, periodMs: 60_000 }) });
export async function loader({ request }) { const headers = await rateLimit(request); return json(data, { headers }); }
// Elysia — an onBeforeHandle hook
import { elysiaRateLimit } from "throttlekit/elysia";
new Elysia().onBeforeHandle(elysiaRateLimit({ strategy: gcra({ limit: 30, periodMs: 10_000 }) })).get("/", () => "ok");No adapter for your transport? createEnforcer is the dependency-free core every binding is built on: it turns a key into a verdict + standards headers with the fail policy folded in, so it never throws on a store outage.
import { createEnforcer, gcra } from "throttlekit";
const { enforce } = createEnforcer({ strategy: gcra({ limit: 30, periodMs: 10_000 }) });
const r = await enforce(keyFromMessage(msg)); // { allowed, outcome: "ok" | "limited" | "error", headers, retryAfterMs }
if (!r.allowed) drop(msg); // queue consumer, job runner, custom protocol, …- Operations — standards headers, trusted-proxy IP keys, PII-safe HMAC keys.
- Distributed & provable — back any adapter with Redis, Postgres, Cloudflare, DynamoDB, or Deno KV.
ThrottleKit · MIT · 1.0 — API frozen under SemVer (Stability)
- Getting Started
- Choosing a strategy
- Frameworks & the edge
- Distributed & provable
- Federation
- Scaling & the Fleet
- Unified admission
- Pillar 4 — Weighted Fair Escrow
- Middleware integration
- Distributed adaptive concurrency
- Advanced limiting
- Overload, fairness & DDoS
- Operations
- Monitoring — ThrottleKit Lens
- Policy Plans
- Replay
- Performance
- Migrating
- Polyglot & Python
- GALE & TALE