Better Auth uses traditional cookie-based session management. A session token is stored in a cookie and sent to the server on every request. The server verifies the session and returns user data if the session is valid.
Session table fields
The session table stores the following information:
id — Unique session identifier.
token — The session token, also used as the session cookie value.
userId — The associated user’s ID.
expiresAt — Session expiry date.
ipAddress — Client IP address (from the request).
userAgent — Client user agent header.
Session expiry
Sessions expire after 7 days by default. When a session is used and the updateAge threshold is reached, the expiry is extended by expiresIn.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // extend expiry once per day
},
});
Disable session refresh
Prevent the session expiry from being extended on use:
export const auth = betterAuth({
session: {
disableSessionRefresh: true,
},
});
Defer session refresh
By default, GET /get-session performs a database write to refresh the session, which can cause issues with read-replica setups. When deferred, GET becomes read-only and returns needsRefresh: true when a refresh is needed. The client automatically calls POST to perform the refresh.
export const auth = betterAuth({
session: {
deferSessionRefresh: true,
},
});
Session freshness
Some endpoints require a fresh session — one whose createdAt is within the freshAge limit. The default freshAge is 1 day.
export const auth = betterAuth({
session: {
freshAge: 60 * 5, // session is fresh if created within the last 5 minutes
},
});
To disable the freshness check entirely:
export const auth = betterAuth({
session: {
freshAge: 0,
},
});
Managing sessions
Get session
import { authClient } from "@/lib/auth-client";
const { data: session } = await authClient.getSession();
Use session (reactive)
useSession provides a reactive way to access the current session in framework-specific clients:
const { data: session } = authClient.useSession();
List sessions
Returns all active sessions for the current user:
const sessions = await authClient.listSessions();
Revoke a session
Revoke a specific session by its token:
await authClient.revokeSession({
token: "session-token",
});
Revoke other sessions
Revoke all sessions except the current one:
await authClient.revokeOtherSessions();
Revoke all sessions
await authClient.revokeSessions();
Update session
If you have additional fields configured on the session, update them with updateSession. Core fields (token, userId, expiresAt, etc.) cannot be changed through this endpoint.
await authClient.updateSession({
theme: "dark",
language: "en",
});
Server-side:
await auth.api.updateSession({
body: { theme: "dark" },
headers: await headers(),
});
Revoke sessions on password change
await authClient.changePassword({
newPassword: newPassword,
currentPassword: currentPassword,
revokeOtherSessions: true,
});
Cookie cache
Calling the database on every useSession or getSession call is expensive. Cookie caching stores session data in a short-lived, signed cookie — similar to a JWT access token paired with a refresh token. The server validates the cookie locally instead of querying the database, and a short maxAge ensures session data is refreshed regularly.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
session: {
cookieCache: {
enabled: true,
maxAge: 5 * 60, // cache duration in seconds
},
},
});
When cookieCache is enabled, revoked sessions may remain active on other devices until the cache expires. For immediate revocation, disable cookieCache or set a shorter maxAge.
Cookie cache encoding strategies
| Strategy | Size | Readable | Interoperable | Use case |
|---|
compact (default) | Smallest | Yes | No | Performance-critical, internal use |
jwt | Medium | Yes | Yes | JWT compatibility, external integrations |
jwe | Largest | No | Yes | Sensitive data, maximum security |
All strategies use HMAC-SHA256 or stronger and prevent tampering.
export const auth = betterAuth({
session: {
cookieCache: {
enabled: true,
maxAge: 5 * 60,
strategy: "compact", // or "jwt" or "jwe"
},
},
});
To bypass the cookie cache for a single request and force a database fetch:
const session = await authClient.getSession({
query: { disableCookieCache: true },
});
Server-side:
await auth.api.getSession({
query: { disableCookieCache: true },
headers: await headers(),
});
Sessions in secondary storage
When you configure a secondary storage, Better Auth stores sessions there instead of the primary database.
Force database storage
To store sessions in the primary database even when secondary storage is configured:
export const auth = betterAuth({
secondaryStorage: { /* your implementation */ },
session: {
storeSessionInDatabase: true,
},
});
Preserve revoked sessions
By default, revoking a session removes it from secondary storage. Set preserveSessionInDatabase to keep a record of revoked sessions in the database:
export const auth = betterAuth({
secondaryStorage: { /* your implementation */ },
session: {
preserveSessionInDatabase: true,
},
});
Stateless session management
Better Auth supports fully stateless sessions — session data is stored in a signed or encrypted cookie, and the server never queries a database to validate it.
Basic stateless setup
If you omit the database option, Better Auth automatically enables stateless mode:
import { betterAuth } from "better-auth";
export const auth = betterAuth({
// no database configuration
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
},
},
});
To manually enable stateless mode:
export const auth = betterAuth({
session: {
cookieCache: {
enabled: true,
maxAge: 7 * 24 * 60 * 60, // 7 days
strategy: "jwe",
refreshCache: true,
},
},
account: {
storeStateStrategy: "cookie",
storeAccountCookie: true,
},
});
Automatic refresh with refreshCache
The refreshCache option controls automatic cookie renewal without a database query:
false (default) — No automatic refresh; expires when maxAge is reached.
true — Refreshes automatically when 80% of maxAge has elapsed.
object — Custom configuration with an updateAge property.
export const auth = betterAuth({
session: {
cookieCache: {
enabled: true,
maxAge: 300, // 5 minutes
refreshCache: {
updateAge: 60, // refresh when 60 seconds remain
},
},
},
});
Versioning stateless sessions
Stateless sessions cannot be individually invalidated. To invalidate all sessions at once, increment the version value and redeploy:
export const auth = betterAuth({
session: {
cookieCache: {
version: "2", // increment to invalidate all existing sessions
},
},
});
Changing the version immediately invalidates all existing sessions. All users will be signed out.
Stateless with secondary storage
Combine stateless cookie validation with secondary storage for session revocation support:
import { betterAuth } from "better-auth";
import { redis } from "./redis";
export const auth = betterAuth({
// no primary database
secondaryStorage: {
get: async (key) => await redis.get(key),
set: async (key, value, ttl) => await redis.set(key, value, "EX", ttl),
delete: async (key) => await redis.del(key),
},
session: {
cookieCache: {
maxAge: 5 * 60,
refreshCache: false,
},
},
});
This setup uses cookies for fast validation, Redis for session storage, and supports session revocation by removing entries from Redis.
Customizing the session response
Use the customSession plugin to extend the data returned by getSession and useSession:
import { customSession } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
customSession(async ({ user, session }) => {
const roles = await findUserRoles(session.session.userId);
return {
roles,
user: {
...user,
newField: "newField",
},
session,
};
}),
],
});
To get type inference on the client, pair it with the customSessionClient plugin:
import { customSessionClient } from "better-auth/client/plugins";
import type { auth } from "@/lib/auth";
const authClient = createAuthClient({
plugins: [customSessionClient<typeof auth>()],
});
const { data } = authClient.useSession();
// data.roles
// data.user.newField
Custom session fields are not included in cookie or secondary storage caches. Your custom session function is called every time a session is fetched.