secondaryStorage not deleting session in active-sessions after signing out #1649

Closed
opened 2026-03-13 08:53:28 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @StashOfCode on GitHub (Aug 6, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Add Redis as secondaryStorage
  2. Sign in / Sign out multiple times
  3. Execute a GET active-sessions-{user_id} command and inspect the result
Image

Current vs. Expected behavior

A session in active-sessions is not deleted on signout. Items in active-sessions keep infinitely growing until expiration.

signOut calls deleteSession, but there's no code that deletes a signed out session from active-sessions

https://github.com/better-auth/better-auth/blob/e57a49f99dbf72d462d94e8f8ccaec747eb1a725/packages/better-auth/src/db/internal-adapter.ts#L447-L458

What version of Better Auth are you using?

1.3.4

Provide environment information

-

Which area(s) are affected? (Select all that apply)

Backend, Client

Auth config (if applicable)

export const auth = betterAuth({
  appName: "App",
  baseURL: process.env.NEXT_PUBLIC_BASE_URL!,
  database: drizzleAdapter(db, {
    provider: "pg",
    schema,
  }),
  secondaryStorage: redisSecondaryStorage,
  emailAndPassword: {
    enabled: true,
    autoSignIn: false,
    requireEmailVerification: true,
    resetPasswordTokenExpiresIn: 600, // 10 minutes
    // minPasswordLength: 8,
    // maxPasswordLength: 24,
    sendResetPassword: async ({ user, url }) => {
      // TODO: wrap in try-catch to handle errors?
      resend.emails.send({
        from: process.env.RESEND_FROM_EMAIL as string,
        to: user.email,
        subject: "Reset your password",
        react: ForgotPasswordEmail({
          userEmail: user.email,
          resetUrl: url,
        }),
      });
    },
    // onPasswordReset: async ({ user }, request) => {
    //   // your logic here
    //   console.log(`Password for user ${user.email} has been reset.`);
    // },
  },
  emailVerification: {
    sendVerificationEmail: async ({ user, url }) => {
      // TODO: wrap in try-catch to handle errors?
      resend.emails.send({
        from: process.env.RESEND_FROM_EMAIL as string,
        to: user.email,
        subject: "Verify your email",
        react: VerifyEmail({
          userEmail: user.email,
          verificationUrl: url
        }),
      })
    },
    sendOnSignUp: true,
    autoSignInAfterVerification: true,
    expiresIn: 3600, // 1 hour
  },
  user: {
    changeEmail: {
      enabled: false,
      // sendChangeEmailVerification: async ({ user, newEmail, url, token }, request) => {}
    }
  },
  account: {
    accountLinking: {
      enabled: true,
    },
  },
  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 days (default - 7 days)
    cookieCache: {
      enabled: true,
      maxAge: 60 * 5, // 5 minutes
    },
    disableSessionRefresh: false, // default - false
    storeSessionInDatabase: false, // Store session in database when secondary storage is provided (default: `false`)
		preserveSessionInDatabase: false, // Preserve session records in database when deleted from secondary storage (default: `false`)
  },
  
  socialProviders: {
    linkedin: {
      clientId: process.env.LINKEDIN_CLIENT_ID as string,
      clientSecret: process.env.LINKEDIN_CLIENT_SECRET as string,
    },
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID as string,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
    },
  },
  hooks: {},
  databaseHooks: {},
  // rateLimit: {  // enabled in prod by default
  //   enabled: true,
  //   storage: 'secondary-storage',
  // },
  advanced: {
    cookiePrefix: "app",
    ipAddress: {
      disableIpTracking: false, // default - false
    },
  },
  plugins: [nextCookies()],  // make sure this is the last plugin in the array
});

Additional context

No response

Originally created by @StashOfCode on GitHub (Aug 6, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Add Redis as `secondaryStorage` 2. Sign in / Sign out multiple times 3. Execute a `GET active-sessions-{user_id}` command and inspect the result <img width="394" height="510" alt="Image" src="https://github.com/user-attachments/assets/570dd73f-a3d1-4e03-a1b1-be08698fb8ba" /> ### Current vs. Expected behavior A session in `active-sessions` is not deleted on signout. Items in `active-sessions` keep infinitely growing until expiration. `signOut` calls `deleteSession`, but there's no code that deletes a signed out session from `active-sessions` https://github.com/better-auth/better-auth/blob/e57a49f99dbf72d462d94e8f8ccaec747eb1a725/packages/better-auth/src/db/internal-adapter.ts#L447-L458 ### What version of Better Auth are you using? 1.3.4 ### Provide environment information ```bash - ``` ### Which area(s) are affected? (Select all that apply) Backend, Client ### Auth config (if applicable) ```typescript export const auth = betterAuth({ appName: "App", baseURL: process.env.NEXT_PUBLIC_BASE_URL!, database: drizzleAdapter(db, { provider: "pg", schema, }), secondaryStorage: redisSecondaryStorage, emailAndPassword: { enabled: true, autoSignIn: false, requireEmailVerification: true, resetPasswordTokenExpiresIn: 600, // 10 minutes // minPasswordLength: 8, // maxPasswordLength: 24, sendResetPassword: async ({ user, url }) => { // TODO: wrap in try-catch to handle errors? resend.emails.send({ from: process.env.RESEND_FROM_EMAIL as string, to: user.email, subject: "Reset your password", react: ForgotPasswordEmail({ userEmail: user.email, resetUrl: url, }), }); }, // onPasswordReset: async ({ user }, request) => { // // your logic here // console.log(`Password for user ${user.email} has been reset.`); // }, }, emailVerification: { sendVerificationEmail: async ({ user, url }) => { // TODO: wrap in try-catch to handle errors? resend.emails.send({ from: process.env.RESEND_FROM_EMAIL as string, to: user.email, subject: "Verify your email", react: VerifyEmail({ userEmail: user.email, verificationUrl: url }), }) }, sendOnSignUp: true, autoSignInAfterVerification: true, expiresIn: 3600, // 1 hour }, user: { changeEmail: { enabled: false, // sendChangeEmailVerification: async ({ user, newEmail, url, token }, request) => {} } }, account: { accountLinking: { enabled: true, }, }, session: { expiresIn: 60 * 60 * 24 * 7, // 7 days (default - 7 days) cookieCache: { enabled: true, maxAge: 60 * 5, // 5 minutes }, disableSessionRefresh: false, // default - false storeSessionInDatabase: false, // Store session in database when secondary storage is provided (default: `false`) preserveSessionInDatabase: false, // Preserve session records in database when deleted from secondary storage (default: `false`) }, socialProviders: { linkedin: { clientId: process.env.LINKEDIN_CLIENT_ID as string, clientSecret: process.env.LINKEDIN_CLIENT_SECRET as string, }, google: { clientId: process.env.GOOGLE_CLIENT_ID as string, clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, }, }, hooks: {}, databaseHooks: {}, // rateLimit: { // enabled in prod by default // enabled: true, // storage: 'secondary-storage', // }, advanced: { cookiePrefix: "app", ipAddress: { disableIpTracking: false, // default - false }, }, plugins: [nextCookies()], // make sure this is the last plugin in the array }); ``` ### Additional context _No response_
GiteaMirror added the bug label 2026-03-13 08:53:28 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Aug 6, 2025):

Hi @StashOfCode! I'm Dosu and I’m helping the better-auth team.

You're correct—Better Auth v1.3.4 does not remove signed-out sessions from the active-sessions-{userId} list in secondaryStorage (Redis). The deleteSession function deletes the session token itself but doesn't update the active-sessions list, so stale session references accumulate until they expire. This is a known issue and not intended behavior, as confirmed by other users and maintainers (example).

Currently, the recommended workaround is to manually remove the session token from the active-sessions-{userId} list in Redis when signing out. You can do this by using a custom hook or direct Redis command. Maintainers have suggested using the hooks API or updating Redis directly for now (example). There isn't a built-in fix in the latest release, but session management improvements are ongoing.

If you want to automate cleanup, consider adding a signOut hook that removes the session token from the relevant active-sessions list in Redis. Let me know if you want a code example for that workaround.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

@dosubot[bot] commented on GitHub (Aug 6, 2025): <!-- Greeting --> Hi @StashOfCode! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> You're correct—Better Auth v1.3.4 does not remove signed-out sessions from the `active-sessions-{userId}` list in secondaryStorage (Redis). The `deleteSession` function deletes the session token itself but doesn't update the `active-sessions` list, so stale session references accumulate until they expire. This is a known issue and not intended behavior, as confirmed by other users and maintainers ([example](https://github.com/better-auth/better-auth/issues/1641)). Currently, the recommended workaround is to manually remove the session token from the `active-sessions-{userId}` list in Redis when signing out. You can do this by using a custom hook or direct Redis command. Maintainers have suggested using the hooks API or updating Redis directly for now ([example](https://github.com/better-auth/better-auth/issues/2127)). There isn't a built-in fix in the latest release, but session management improvements are ongoing. If you want to automate cleanup, consider adding a `signOut` hook that removes the session token from the relevant `active-sessions` list in Redis. Let me know if you want a code example for that workaround. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/281ccd5b-84b2-4598-b06d-fb03165a06d0?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/281ccd5b-84b2-4598-b06d-fb03165a06d0?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/281ccd5b-84b2-4598-b06d-fb03165a06d0?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/281ccd5b-84b2-4598-b06d-fb03165a06d0?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/281ccd5b-84b2-4598-b06d-fb03165a06d0?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/281ccd5b-84b2-4598-b06d-fb03165a06d0?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/281ccd5b-84b2-4598-b06d-fb03165a06d0?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3819)
Author
Owner

@dosubot[bot] commented on GitHub (Aug 6, 2025):

Here's a production-ready TypeScript example for cleaning up Redis after signout using a Better Auth global after hook. This assumes you're using ioredis or node-redis and have access to your Redis client instance.

Add this to your Better Auth config:

import { parse } from "cookie"; // or use your framework's cookie parser

// Make sure you have your Redis client instance available
// Example: import { redis } from "./redis"; // ioredis or node-redis

export const auth = betterAuth({
  // ...your existing config
  hooks: {
    after: async (ctx) => {
      // Only run on signOut endpoint
      if (ctx.path !== "/api/auth/sign-out" || ctx.method !== "POST") return;

      // Get session token from cookies (signed or unsigned, depending on your setup)
      const cookieHeader = ctx.request?.headers?.get("cookie");
      if (!cookieHeader) return;

      const cookies = parse(cookieHeader);
      const sessionToken = cookies["app.session-token"]; // adjust if you use a different cookie name/prefix

      if (!sessionToken) return;

      // Look up session in Redis to get userId
      const sessionRaw = await redis.get(sessionToken);
      if (!sessionRaw) return;

      let userId: string | undefined;
      try {
        const sessionObj = JSON.parse(sessionRaw);
        userId = sessionObj.session?.userId;
      } catch {
        return;
      }
      if (!userId) return;

      // Remove the session token from the user's active-sessions list
      const key = `active-sessions-${userId}`;
      const listRaw = await redis.get(key);
      if (!listRaw) return;

      let list: { token: string; expiresAt: number }[] = [];
      try {
        list = JSON.parse(listRaw);
      } catch {
        return;
      }

      const filtered = list.filter((s) => s.token !== sessionToken);
      if (filtered.length !== list.length) {
        await redis.set(key, JSON.stringify(filtered));
      }
    },
  },
});

This hook runs after every request, but only does work on the sign-out endpoint. It parses the session token from cookies, fetches the session from Redis to get the userId, then removes the token from the user's active-sessions list. Adjust the cookie name if you use a custom prefix.

This is idiomatic, safe, and should work with both ioredis and node-redis. If you use a different Redis client, adapt the get/set calls as needed.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

@dosubot[bot] commented on GitHub (Aug 6, 2025): <!-- Answer --> Here's a production-ready TypeScript example for cleaning up Redis after signout using a Better Auth global after hook. This assumes you're using ioredis or node-redis and have access to your Redis client instance. Add this to your Better Auth config: ```typescript import { parse } from "cookie"; // or use your framework's cookie parser // Make sure you have your Redis client instance available // Example: import { redis } from "./redis"; // ioredis or node-redis export const auth = betterAuth({ // ...your existing config hooks: { after: async (ctx) => { // Only run on signOut endpoint if (ctx.path !== "/api/auth/sign-out" || ctx.method !== "POST") return; // Get session token from cookies (signed or unsigned, depending on your setup) const cookieHeader = ctx.request?.headers?.get("cookie"); if (!cookieHeader) return; const cookies = parse(cookieHeader); const sessionToken = cookies["app.session-token"]; // adjust if you use a different cookie name/prefix if (!sessionToken) return; // Look up session in Redis to get userId const sessionRaw = await redis.get(sessionToken); if (!sessionRaw) return; let userId: string | undefined; try { const sessionObj = JSON.parse(sessionRaw); userId = sessionObj.session?.userId; } catch { return; } if (!userId) return; // Remove the session token from the user's active-sessions list const key = `active-sessions-${userId}`; const listRaw = await redis.get(key); if (!listRaw) return; let list: { token: string; expiresAt: number }[] = []; try { list = JSON.parse(listRaw); } catch { return; } const filtered = list.filter((s) => s.token !== sessionToken); if (filtered.length !== list.length) { await redis.set(key, JSON.stringify(filtered)); } }, }, }); ``` This hook runs after every request, but only does work on the sign-out endpoint. It parses the session token from cookies, fetches the session from Redis to get the userId, then removes the token from the user's active-sessions list. Adjust the cookie name if you use a custom prefix. This is idiomatic, safe, and should work with both ioredis and node-redis. If you use a different Redis client, adapt the get/set calls as needed. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/8b31a527-a160-4e8e-8b0a-41c5e1ccc8bb?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/8b31a527-a160-4e8e-8b0a-41c5e1ccc8bb?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/8b31a527-a160-4e8e-8b0a-41c5e1ccc8bb?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/8b31a527-a160-4e8e-8b0a-41c5e1ccc8bb?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/8b31a527-a160-4e8e-8b0a-41c5e1ccc8bb?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/8b31a527-a160-4e8e-8b0a-41c5e1ccc8bb?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/8b31a527-a160-4e8e-8b0a-41c5e1ccc8bb?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3819)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1649