Rate limiter plugin for Better Auth — rate limit any application route with memory, database, or Redis-backed storage. Community plugin.
- Rate limit any route by IP address, authenticated user, or both
- Three storage backends: in-memory, database, or secondary storage (Redis)
- Per-path custom rules with wildcard pattern support (
*,**) - Disable rate limiting for specific paths
- Standard HTTP response headers (
X-RateLimit-*) - Full TypeScript support
npm install better-auth-rate-limiter
# or
pnpm add better-auth-rate-limiterAdd the plugin to your Better Auth instance:
import { betterAuth } from "better-auth";
import { rateLimiter } from "better-auth-rate-limiter";
export const auth = betterAuth({
// ...your config
plugins: [
rateLimiter({
window: 60, // Time window in seconds (default: 60)
max: 100, // Max requests per window (default: 100)
storage: "memory", // Storage backend (default: "memory")
detection: "ip", // Detection mode (default: "ip")
}),
],
});import { createAuthClient } from "better-auth/client";
import { rateLimiterClient } from "better-auth-rate-limiter/client";
export const authClient = createAuthClient({
plugins: [rateLimiterClient()],
});Fast in-process storage. Does not persist across restarts and is not shared between multiple server instances.
rateLimiter({
storage: "memory",
})Persists rate limit data in your existing Better Auth database. Automatically creates a rateLimit table.
rateLimiter({
storage: "database",
})Use a Redis-compatible store configured via Better Auth's secondaryStorage option.
import { betterAuth } from "better-auth";
import { rateLimiter } from "better-auth-rate-limiter";
import { Redis } from "ioredis";
const redis = new Redis();
export const auth = betterAuth({
secondaryStorage: {
get: (key) => redis.get(key),
set: (key, value, ttl) => redis.set(key, value, "EX", ttl ?? 3600),
delete: (key) => redis.del(key),
},
plugins: [
rateLimiter({
storage: "secondary-storage",
window: 60,
max: 100,
}),
],
});Rate limit by the client's IP address (respects x-forwarded-for).
rateLimiter({ detection: "ip" })Rate limit by authenticated user ID. Unauthenticated requests are not rate limited.
rateLimiter({ detection: "user" })Use the authenticated user's ID when available, fall back to IP for unauthenticated requests.
rateLimiter({ detection: "ip-and-user" })Override the default limits for specific paths. Supports * (single segment) and ** (multi-segment) wildcards.
rateLimiter({
window: 60,
max: 100,
customRules: {
// Stricter limit for login
"/api/auth/sign-in": { window: 60, max: 5 },
// Very strict for sign-up
"/api/auth/sign-up": { window: 3600, max: 3 },
// Stricter for all AI endpoints
"/api/ai/*": { window: 60, max: 10 },
// Even stricter for a specific AI endpoint
"/api/ai/generate": { window: 3600, max: 5 },
// Disable rate limiting for health checks
"/api/health": false,
},
})Use auth.api.checkRateLimit() to enforce rate limits inside your route handlers.
// src/app/api/some-route/route.ts
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
export async function GET(request: NextRequest) {
const result = await auth.api.checkRateLimit({
headers: request.headers,
body: { path: request.nextUrl.pathname },
});
if (!result.success) {
return NextResponse.json(
{
error: "Too many requests",
message: result.message,
retryAfter: result.retryAfter,
},
{
status: 429,
headers: {
"Retry-After": String(result.retryAfter),
"X-RateLimit-Limit": String(result.limit),
"X-RateLimit-Remaining": "0",
},
},
);
}
// Your route logic here
return NextResponse.json({ data: "..." });
}// src/lib/rate-limit.ts
import { NextRequest, NextResponse } from "next/server";
import { auth } from "./auth";
export async function withRateLimit(request: NextRequest) {
const path = request.nextUrl.pathname;
const result = await auth.api.checkRateLimit({
headers: request.headers,
body: { path },
});
if (!result.success) {
return NextResponse.json(
{
error: "Too many requests",
message: result.message,
retryAfter: result.retryAfter,
},
{
status: 429,
headers: {
"Retry-After": String(result.retryAfter),
"X-RateLimit-Limit": String(result.limit),
"X-RateLimit-Remaining": "0",
},
},
);
}
return { limited: false, ...result };
}Apply rate limiting globally to all API routes:
// src/middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
export async function middleware(request: NextRequest) {
const path = request.nextUrl.pathname;
// Skip Better Auth's own routes (they are handled separately)
if (path.startsWith("/api/auth")) {
return NextResponse.next();
}
const result = await auth.api.checkRateLimit({
headers: request.headers,
body: { path },
});
if (!result.success) {
return NextResponse.json(
{ error: "Too many requests", retryAfter: result.retryAfter },
{
status: 429,
headers: { "Retry-After": String(result.retryAfter) },
},
);
}
// Attach rate limit info to response headers
const response = NextResponse.next();
response.headers.set("X-RateLimit-Limit", String(result.limit));
response.headers.set("X-RateLimit-Remaining", String(result.remaining));
if (result.resetAt) {
response.headers.set(
"X-RateLimit-Reset",
String(Math.ceil(result.resetAt / 1000)), // Unix seconds
);
}
return response;
}
export const config = {
matcher: ["/api/:path*"],
};| Option | Type | Default | Description |
|---|---|---|---|
window |
number |
60 |
Time window in seconds |
max |
number |
100 |
Maximum requests per window |
storage |
"memory" | "database" | "secondary-storage" |
"memory" |
Storage backend |
detection |
"ip" | "user" | "ip-and-user" |
"ip" |
How to identify clients |
customRules |
Record<string, { window: number; max: number } | false> |
— | Per-path rule overrides |
| Field | Type | Description |
|---|---|---|
success |
boolean |
Whether the request is allowed |
limit |
number |
Max requests for this window |
remaining |
number |
Requests remaining in window |
retryAfter |
number | undefined |
Seconds until the limit resets (only when rate limited) |
resetAt |
number | undefined |
Unix timestamp (ms) when the window resets |
message |
string | undefined |
Human-readable error message (only when rate limited) |
MIT