Extending Better Auth with global Rate Limiting

Published: (February 20, 2026 at 03:33 PM EST)
4 min read
Source: Dev.to

Source: Dev.to

TL;DR

Install better-auth-rate-limiter, add a single plugin call, and every route in your app—auth endpoints, AI routes, payment APIs, etc.—is rate‑limited. Choose memory, database, or Redis as the backend. Full TypeScript support.

Installation

npm install better-auth-rate-limiter
# or
pnpm add better-auth-rate-limiter

Adding the Plugin

import { betterAuth } from "better-auth";
import { rateLimiter } from "better-auth-rate-limiter";

export const auth = betterAuth({
  plugins: [
    rateLimiter({
      window: 60,       // seconds
      max: 100,         // requests per window
      storage: "memory",
      detection: "ip",
    }),
  ],
});

The auth instance now acts as the central rate‑limiting authority for all routes you point it at.

Storage Backends

Memory (default)

rateLimiter({ storage: "memory" })

Zero dependencies, works immediately. Ideal for single‑instance apps or development.

Database

rateLimiter({ storage: "database" })

Persists rate‑limit state across restarts and works across multiple server instances using your existing Better Auth database.

Redis (secondary storage)

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" }),
  ],
});

Best for high‑traffic production apps or horizontally scaled deployments.

Detection Modes

// IP‑based (default, for unauthenticated traffic)
rateLimiter({ detection: "ip" })

// User‑based (authenticated users only)
rateLimiter({ detection: "user" })

// Both: user ID when logged in, IP when not
rateLimiter({ detection: "ip-and-user" })

ip-and-user provides stricter limits for authenticated users while still protecting anonymous traffic.

Custom Rules & Wildcards

rateLimiter({
  window: 60,
  max: 100,
  customRules: {
    // Brute‑force protection for sign‑in
    "/api/auth/sign-in": { window: 60, max: 5 },

    // Bot protection for sign‑up
    "/api/auth/sign-up": { window: 3600, max: 3 },

    // Limit all AI endpoints
    "/api/ai/*": { window: 60, max: 10 },

    // Disable rate limiting for health checks
    "/api/health": false,
  },
})

Wildcard support: * matches a single segment, ** matches multiple segments.

Using checkRateLimit in Route Handlers

// src/app/api/generate/route.ts
import { auth } from "@/lib/auth";
import { NextRequest, NextResponse } from "next/server";

export async function POST(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", retryAfter: result.retryAfter },
      { status: 429 }
    );
  }

  // Your protected logic (LLM call, payment, etc.)
}

The path you provide is matched against customRules, allowing per‑endpoint limits without extra setup.

Middleware Integration

// src/middleware.ts
import { auth } from "@/lib/auth";
import { NextRequest, NextResponse } from "next/server";

export async function middleware(request: NextRequest) {
  const path = request.nextUrl.pathname;

  // Skip Better Auth's own routes (already handled)
  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 }
    );
  }

  const response = NextResponse.next();
  response.headers.set("X-RateLimit-Limit", String(result.limit));
  response.headers.set("X-RateLimit-Remaining", String(result.remaining));
  return response;
}

export const config = {
  matcher: ["/api/:path*"],
};

All API requests are checked before reaching route handlers.

Response Headers

Every successful response includes:

  • X-RateLimit-Limit – total allowed requests per window
  • X-RateLimit-Remaining – requests left in the current window
  • X-RateLimit-Reset (if implemented) – time until the window resets

Clients can read these headers to avoid guessing.

Features Summary

FeatureDetails
Storage backendsmemory, database, Redis (via secondary storage)
Detection modesIP, user, or both (ip-and-user)
Per‑route custom rulesWildcard support (*, **)
Standard rate‑limit headersX-RateLimit-* automatically added
TypeScriptFull type safety
LicenseMIT (community plugin)

Have feedback or a use case not covered? Open an issue on GitHub.

0 views
Back to Blog

Related posts

Read more »