This guide covers the most impactful techniques for improving the performance
of a Better Auth application.
Session caching with cookies
By default, every call to getSession or useSession hits the database.
Cookie caching stores a short-lived signed copy of the session in the browser,
eliminating the database round-trip for repeated reads.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
session: {
cookieCache: {
enabled: true,
maxAge: 5 * 60, // 5 minutes
},
},
});
This is analogous to using a short-lived JWT access token alongside a refresh
token. The session is re-validated from the database after maxAge seconds.
Read more in the session management docs.
Framework-level caching
Next.js
React Router
SolidStart
TanStack Query
Use the "use cache" directive (Next.js 15+) on server functions that
return user lists or other infrequently-changing data:export async function getUsers() {
"use cache";
const { users } = await auth.api.listUsers();
return users;
}
Learn more in the Next.js use cache docs. Return Cache-Control headers from loaders:import { data } from "react-router";
import type { Route } from "./+types/your-route";
export const loader = async ({ request }: Route.LoaderArgs) => {
const { users } = await auth.api.listUsers();
return data(users, {
headers: { "Cache-Control": "max-age=3600" },
});
};
export function headers({ loaderHeaders }: Route.HeadersArgs) {
return loaderHeaders;
}
Wrap API calls with SolidStart’s query primitive:import { query } from "@solidjs/router";
const getUsers = query(
async () => (await auth.api.listUsers()).users,
"getUsers",
);
Use staleTime to cache data in memory:import { useQuery } from "@tanstack/react-query";
export function useUsers() {
return useQuery({
queryKey: ["users"],
queryFn: async () => (await auth.api.listUsers()).users,
staleTime: 1000 * 60 * 15, // 15 minutes
});
}
SSR session prefetching
Pre-fetch the session on the server and pass it as initial data to the client
to avoid a waterfall request:
// In a server component or loader
const session = await auth.api.getSession({
headers: await headers(),
});
// Pass `session` as a prop or via a data store to the client
Background tasks
On serverless platforms, non-critical work like cleanup, analytics, and email
can run after the response is sent using the backgroundTasks option. This
reduces perceived latency without sacrificing correctness.
import { betterAuth } from "better-auth";
import { createAuthMiddleware } from "better-auth/api";
import { waitUntil } from "@vercel/functions";
export const auth = betterAuth({
advanced: {
backgroundTasks: { handler: waitUntil },
},
hooks: {
after: createAuthMiddleware(async (ctx) => {
if (ctx.path === "/sign-up/email") {
ctx.context.runInBackground(
sendWelcomeEmail(ctx.context.newSession?.user.id),
);
}
}),
},
});
See the backgroundTasks option and the
hooks docs for Cloudflare Workers examples.
Background tasks introduce eventual consistency: the response returns before
deferred work completes. Only use this when your application can tolerate
brief inconsistency.
Database indexing
Adding indexes to the core tables has the highest impact at scale. The table
below lists the fields that benefit most from indexes:
| Table | Fields to index | Plugin |
|---|
users | email | — |
accounts | userId | — |
sessions | userId, token | — |
verifications | identifier | — |
invitations | email, organizationId | organization |
members | userId, organizationId | organization |
organizations | slug | organization |
passkey | userId | passkey |
twoFactor | secret | two-factor |
Index support in the generate / migrate CLI commands is planned for a
future release.
Bundle size optimization
If you are using a custom ORM adapter (Prisma, Drizzle, MongoDB) you can
reduce your server bundle by importing from better-auth/minimal. This
variant omits the bundled Kysely dependency.
import { betterAuth } from "better-auth/minimal";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export const auth = betterAuth({
database: prismaAdapter(prisma, { provider: "postgresql" }),
});
import { betterAuth } from "better-auth/minimal";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "./database";
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "pg" }),
});
import { betterAuth } from "better-auth/minimal";
import { mongodbAdapter } from "better-auth/adapters/mongodb";
import { MongoClient } from "mongodb";
const client = new MongoClient(process.env.DATABASE_URL!);
export const auth = betterAuth({
database: mongodbAdapter(client.db()),
});
better-auth/minimal does not support direct database connections or
built-in migrations. Use a full better-auth import if you need those
features.
Rate limiting
Better Auth includes built-in rate limiting. In high-traffic scenarios, using
secondaryStorage (Redis, Cloudflare KV) for rate-limit counters instead of
in-memory storage avoids state loss across serverless invocations:
rateLimit: {
enabled: true,
storage: "secondary-storage",
window: 10,
max: 100,
customRules: {
"/sign-in/email": { window: 10, max: 5 },
},
}