DEV Community

Cover image for Extending Better Auth with global Rate Limiting
Lutz
Lutz

Posted on

Extending Better Auth with global Rate Limiting

TL;DR - Install better-auth-rate-limiter, add one plugin call, and every route in your app is rate limited — auth endpoints, AI routes, payment APIs, anything. Memory, database, or Redis backend. Full TypeScript end to end.


Your API is open to the internet. Anyone can hammer your /api/generate, /api/checkout, or /api/auth/sign-in endpoints thousands of times per minute - brute-forcing credentials, abusing expensive AI calls, or just making your app crawl.

Better Auth handles authentication well, but rate limiting is your problem to solve. And it shouldn't apply only to auth routes - your entire API needs protection.

better-auth-rate-limiter is a community plugin that adds flexible, production-ready rate limiting to any route in your app - not just auth endpoints — in a few lines of config.

Installation

npm install better-auth-rate-limiter
# or
pnpm add better-auth-rate-limiter
Enter fullscreen mode Exit fullscreen mode

Basic Setup

Add the plugin to your Better Auth instance:

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

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

That's it. Your auth instance is now the central rate limiting authority — for auth routes and any other route you point at it.

Three Storage Backends

Memory (default)

Zero dependencies, works immediately. Best for single-instance apps or development.

rateLimiter({ storage: "memory" })
Enter fullscreen mode Exit fullscreen mode

Database

Persists rate limit state across restarts and works across multiple server instances. Uses your existing Better Auth database — no extra setup needed.

rateLimiter({ storage: "database" })
Enter fullscreen mode Exit fullscreen mode

Redis

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

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" }),
  ],
});
Enter fullscreen mode Exit fullscreen mode

Three Detection Modes

Rate limit by IP address (default), authenticated user ID, or both:

// IP-based (unauthenticated traffic)
rateLimiter({ detection: "ip" })

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

// Best of both: user ID when logged in, IP when not
rateLimiter({ detection: "ip-and-user" })
Enter fullscreen mode Exit fullscreen mode

"ip-and-user" is great for apps where you want stricter limits for authenticated users without letting anonymous traffic slip through.

Custom Rules Per Route

The global limit is just the baseline. Override it for specific paths using wildcards:

rateLimiter({
  window: 60,
  max: 100,
  customRules: {
    // Stricter for login (brute-force protection)
    "/api/auth/sign-in": { window: 60, max: 5 },

    // Very strict for sign-up (bot protection)
    "/api/auth/sign-up": { window: 3600, max: 3 },

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

    // Disable entirely for health checks
    "/api/health": false,
  },
})
Enter fullscreen mode Exit fullscreen mode

The wildcard support (* for single segment, ** for multi-segment) makes it easy to apply rules to entire areas of your API.

Rate Limit Any API Route

This is the main use case beyond auth. Call checkRateLimit() from any Next.js route handler — AI endpoints, payment flows, form submissions, whatever you need to protect:

// 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 }
    );
  }

  // Call your LLM, charge the card, whatever — safely throttled
}
Enter fullscreen mode Exit fullscreen mode

The path you pass in is matched against your customRules, so you can give /api/generate a much stricter limit than your public endpoints without any extra setup.

Protect Everything at Once with Next.js Middleware

For full-coverage protection, add rate limiting at the middleware level. Every API request gets checked before it hits your route handlers:

// 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 by the plugin
  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*"],
};
Enter fullscreen mode Exit fullscreen mode

Standard Response Headers

Every response includes X-RateLimit-* headers so clients know their current limit, how many requests are left, and when the window resets — no guessing required.

Wrapping Up

better-auth-rate-limiter gives you:

  • Three storage backends — memory, database, Redis
  • Three detection modes — IP, user, or both
  • Per-route custom rules with wildcard support
  • Standard rate limit headers out of the box
  • Full TypeScript support

It's a community plugin, MIT licensed, and takes about 5 minutes to set up.


Have feedback or a use case this doesn't cover? Open an issue on GitHub.

Top comments (0)