Why few keys in redis has no TTL? #1718

Closed
opened 2026-03-13 08:58:13 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @varundeva on GitHub (Aug 15, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Hey im noticing many keys are there in redis which has no TTL, so it stays forever, session information will be expiry after 30 days,

Im using secondary database as redis by passing
secondaryStorage: {
get: async (key) => {
const value = await redis.get(key);
return value ? value : null;
},
set: async (key, value, ttl) => {
if (ttl) await redis.set(key, value, ttl);
else await redis.set(key, value);
},
delete: async (key) => {
await redis.delete(key);
},
},

to auth

Image

Just need to understand how to add ttl to this? and is it bug?
because of this redis is getting huge amout of data,
even when this is used?

Current vs. Expected behavior

Should have TTL for all the keys

What version of Better Auth are you using?

1.3.1

System info

System:
    OS: macOS 15.5
    CPU: (11) arm64 Apple M3 Pro
    Memory: 82.19 MB / 18.00 GB
    Shell: 5.9 - /bin/zsh
  Browsers:
    Chrome: 139.0.7258.128
    Safari: 18.5

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

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth";
import dotenv from "dotenv";
dotenv.config();
import { MongoClient } from "mongodb";
import { mongodbAdapter } from "better-auth/adapters/mongodb";
import { nextCookies } from "better-auth/next-js";
import { admin } from "better-auth/plugins";
import redis from "@/lib/redis";

const client = new MongoClient(
  (process.env.MONGODB_URI as string) || "mongodb://localhost:27017",
);
const db = client.db();

export const auth = betterAuth({
  database: mongodbAdapter(db),
  socialProviders: {
    google: {
      prompt: "select_account",
      clientId: process.env.GOOGLE_CLIENT_ID as string,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
    },
    linkedin: {
      clientId: process.env.LINKEDIN_CLIENT_ID as string,
      clientSecret: process.env.LINKEDIN_CLIENT_SECRET as string,
    },
    tiktok: {
      clientId: process.env.TIKTOK_CLIENT_ID as string,
      clientSecret: process.env.TIKTOK_CLIENT_SECRET as string,
      clientKey: process.env.TIKTOK_CLIENT_KEY as string,
    },
    facebook: {
      clientId: process.env.FACEBOOK_CLIENT_ID as string,
      clientSecret: process.env.FACEBOOK_CLIENT_SECRET as string,
    },
    spotify: {
      clientId: process.env.SPOTIFY_CLIENT_ID as string,
      clientSecret: process.env.SPOTIFY_CLIENT_SECRET as string,
    },
  },
  account: {
    accountLinking: {
      enabled: true,
      trustedProviders: ["google", "linkedin", "facebook", "tiktok", "spotify"],
    },
  },
  plugins: [nextCookies(), admin()],
  secondaryStorage: {
    get: async (key) => {
      const value = await redis.get(key);
      return value ? value : null;
    },
    set: async (key, value, ttl) => {
      if (ttl) await redis.set(key, value, ttl);
      else await redis.set(key, value);
    },
    delete: async (key) => {
      await redis.delete(key);
    },
  },
});

Additional context

This has been created help thread in Discord as well, here is ref https://discord.com/channels/1288403910284935179/1402531236454338641

Originally created by @varundeva on GitHub (Aug 15, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce Hey im noticing many keys are there in redis which has no TTL, so it stays forever, session information will be expiry after 30 days, Im using secondary database as redis by passing secondaryStorage: { get: async (key) => { const value = await redis.get(key); return value ? value : null; }, set: async (key, value, ttl) => { if (ttl) await redis.set(key, value, ttl); else await redis.set(key, value); }, delete: async (key) => { await redis.delete(key); }, }, to auth <img width="1412" height="972" alt="Image" src="https://github.com/user-attachments/assets/0d469c19-ab82-417c-9670-1dbc560d28a8" /> Just need to understand how to add ttl to this? and is it bug? because of this redis is getting huge amout of data, even when this is used? ### Current vs. Expected behavior Should have TTL for all the keys ### What version of Better Auth are you using? 1.3.1 ### System info ```bash System: OS: macOS 15.5 CPU: (11) arm64 Apple M3 Pro Memory: 82.19 MB / 18.00 GB Shell: 5.9 - /bin/zsh Browsers: Chrome: 139.0.7258.128 Safari: 18.5 ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth"; import dotenv from "dotenv"; dotenv.config(); import { MongoClient } from "mongodb"; import { mongodbAdapter } from "better-auth/adapters/mongodb"; import { nextCookies } from "better-auth/next-js"; import { admin } from "better-auth/plugins"; import redis from "@/lib/redis"; const client = new MongoClient( (process.env.MONGODB_URI as string) || "mongodb://localhost:27017", ); const db = client.db(); export const auth = betterAuth({ database: mongodbAdapter(db), socialProviders: { google: { prompt: "select_account", clientId: process.env.GOOGLE_CLIENT_ID as string, clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, }, linkedin: { clientId: process.env.LINKEDIN_CLIENT_ID as string, clientSecret: process.env.LINKEDIN_CLIENT_SECRET as string, }, tiktok: { clientId: process.env.TIKTOK_CLIENT_ID as string, clientSecret: process.env.TIKTOK_CLIENT_SECRET as string, clientKey: process.env.TIKTOK_CLIENT_KEY as string, }, facebook: { clientId: process.env.FACEBOOK_CLIENT_ID as string, clientSecret: process.env.FACEBOOK_CLIENT_SECRET as string, }, spotify: { clientId: process.env.SPOTIFY_CLIENT_ID as string, clientSecret: process.env.SPOTIFY_CLIENT_SECRET as string, }, }, account: { accountLinking: { enabled: true, trustedProviders: ["google", "linkedin", "facebook", "tiktok", "spotify"], }, }, plugins: [nextCookies(), admin()], secondaryStorage: { get: async (key) => { const value = await redis.get(key); return value ? value : null; }, set: async (key, value, ttl) => { if (ttl) await redis.set(key, value, ttl); else await redis.set(key, value); }, delete: async (key) => { await redis.delete(key); }, }, }); ``` ### Additional context This has been created help thread in Discord as well, here is ref https://discord.com/channels/1288403910284935179/1402531236454338641
GiteaMirror added the bug label 2026-03-13 08:58:13 -05:00
Author
Owner

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

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

The reason some Redis keys have no TTL is likely due to how the TTL is being set in your secondaryStorage.set implementation. In most Redis clients (like ioredis or node-redis), you need to explicitly use the 'EX' option to set the TTL in seconds. Your current code, redis.set(key, value, ttl), does not set the expiry—this just stores the value, and the third argument is not interpreted as a TTL by most Redis clients.

To ensure all keys have a TTL, update your set method like this (for ioredis/node-redis):

set: async (key, value, ttl) => {
  if (ttl) {
    await redis.set(key, value, 'EX', ttl); // 'EX' sets expiry in seconds
  } else {
    await redis.set(key, value);
  }
}

or, for some clients, you may need to use an options object:

set: async (key, value, ttl) => {
  if (ttl) {
    await redis.set(key, value, { EX: ttl });
  } else {
    await redis.set(key, value);
  }
}

If TTL is not provided, the key will persist forever. Also, Better Auth only sets TTL if it passes a value for the ttl parameter—if it calls set without a TTL, the key will not expire by design (example, example).

There is also an open PR to improve TTL handling for session and token keys, which may help in the future (see here).

So: update your set method to use the 'EX' option, and you should see TTLs applied to all keys where Better Auth provides a TTL. If you still see keys without TTL, it may be because Better Auth is not passing a TTL for those keys (for example, for permanent or non-expiring data).

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 15, 2025): <!-- Greeting --> Hi @varundeva! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The reason some Redis keys have no TTL is likely due to how the TTL is being set in your secondaryStorage.set implementation. In most Redis clients (like ioredis or node-redis), you need to explicitly use the `'EX'` option to set the TTL in seconds. Your current code, `redis.set(key, value, ttl)`, does not set the expiry—this just stores the value, and the third argument is not interpreted as a TTL by most Redis clients. To ensure all keys have a TTL, update your set method like this (for ioredis/node-redis): ```js set: async (key, value, ttl) => { if (ttl) { await redis.set(key, value, 'EX', ttl); // 'EX' sets expiry in seconds } else { await redis.set(key, value); } } ``` or, for some clients, you may need to use an options object: ```js set: async (key, value, ttl) => { if (ttl) { await redis.set(key, value, { EX: ttl }); } else { await redis.set(key, value); } } ``` If TTL is not provided, the key will persist forever. Also, Better Auth only sets TTL if it passes a value for the ttl parameter—if it calls set without a TTL, the key will not expire by design ([example](https://github.com/better-auth/better-auth/issues/1641#issuecomment-2718622016), [example](https://github.com/better-auth/better-auth/issues/701)). There is also an open PR to improve TTL handling for session and token keys, which may help in the future ([see here](https://github.com/better-auth/better-auth/pull/3836)). So: update your set method to use the `'EX'` option, and you should see TTLs applied to all keys where Better Auth provides a TTL. If you still see keys without TTL, it may be because Better Auth is not passing a TTL for those keys (for example, for permanent or non-expiring data). <!-- 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/32e0397e-0002-44c3-8c6a-13faa62dad42?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/32e0397e-0002-44c3-8c6a-13faa62dad42?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/32e0397e-0002-44c3-8c6a-13faa62dad42?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/32e0397e-0002-44c3-8c6a-13faa62dad42?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/32e0397e-0002-44c3-8c6a-13faa62dad42?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/32e0397e-0002-44c3-8c6a-13faa62dad42?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/32e0397e-0002-44c3-8c6a-13faa62dad42?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/4034)
Author
Owner

@Kinfe123 commented on GitHub (Aug 15, 2025):

make sure to also ref this for more info

@Kinfe123 commented on GitHub (Aug 15, 2025): make sure to also ref [this](https://github.com/redis/node-redis/issues/1000) for more info
Author
Owner

@varundeva commented on GitHub (Aug 15, 2025):

make sure to also ref this for more info

Im using redis package from npm, not node Redis

`import { createClient, type RedisClientType } from "redis";
declare global {
// eslint-disable-next-line no-var
var _redisClient: RedisClientType | undefined;
var _redisConnected: boolean | undefined;
}

let redisClient: RedisClientType;
let isConnected = false;

async function connectRedis() {
if (!process.env.REDIS_URL) {
throw new Error("REDIS_URL environment variable not set");
}

if (!global._redisClient) {
global._redisClient = createClient({
url: process.env.REDIS_URL,
});
global._redisClient.on("error", (err) =>
console.error("Redis Client Error:", err),
);
global._redisClient.on("connect", () =>
console.log("Redis client connected"),
);
global._redisClient.on("end", () =>
console.log("Redis client disconnected"),
);
global._redisConnected = false;
}

redisClient = global._redisClient;
isConnected = global._redisConnected ?? false;

if (!isConnected) {
await redisClient.connect();
isConnected = true;
global._redisConnected = true;
console.log("Redis connected successfully");
}

return redisClient;
}

export const redis = {
get: async (key: string): Promise<string | null> => {
const client = await connectRedis();
return client.get(key);
},
set: async (key: string, value: string, ttl?: number): Promise => {
const client = await connectRedis();
if (ttl) {
await client.set(key, value, { EX: ttl });
} else {
await client.set(key, value);
}
},
delete: async (key: string): Promise => {
const client = await connectRedis();
await client.del(key);
},
quit: async (): Promise => {
if (redisClient && isConnected) {
await redisClient.quit();
isConnected = false;
global._redisConnected = false;
}
},
keys: async (pattern: string): Promise<string[]> => {
const client = await connectRedis();
return client.keys(pattern);
},
exists: async (key: string): Promise => {
const client = await connectRedis();
return (await client.exists(key)) === 1;
},
incr: async (key: string): Promise => {
const client = await connectRedis();
return client.incr(key);
},
expire: async (key: string, seconds: number): Promise => {
const client = await connectRedis();
return (await client.expire(key, seconds)) === 1;
},
};

export default redis;`

auth.ts code is here
`import { betterAuth } from "better-auth";
import dotenv from "dotenv";
dotenv.config();
import { MongoClient } from "mongodb";
import { mongodbAdapter } from "better-auth/adapters/mongodb";
import { nextCookies } from "better-auth/next-js";
import { admin } from "better-auth/plugins";
import redis from "@/lib/redis";

const client = new MongoClient(
(process.env.MONGODB_URI as string) || "mongodb://localhost:27017",
);
const db = client.db();

export const auth = betterAuth({
database: mongodbAdapter(db),
socialProviders: {
google: {
prompt: "select_account",
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
linkedin: {
clientId: process.env.LINKEDIN_CLIENT_ID as string,
clientSecret: process.env.LINKEDIN_CLIENT_SECRET as string,
},
tiktok: {
clientId: process.env.TIKTOK_CLIENT_ID as string,
clientSecret: process.env.TIKTOK_CLIENT_SECRET as string,
clientKey: process.env.TIKTOK_CLIENT_KEY as string,
},
facebook: {
clientId: process.env.FACEBOOK_CLIENT_ID as string,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET as string,
},
spotify: {
clientId: process.env.SPOTIFY_CLIENT_ID as string,
clientSecret: process.env.SPOTIFY_CLIENT_SECRET as string,
},
},
account: {
accountLinking: {
enabled: true,
trustedProviders: ["google", "linkedin", "facebook", "tiktok", "spotify"],
},
},
plugins: [nextCookies(), admin()],
secondaryStorage: {
get: async (key) => {
const value = await redis.get(key);
return value ? value : null;
},
set: async (key, value, ttl) => {
if (ttl) await redis.set(key, value, ttl);
else await redis.set(key, value);
},
delete: async (key) => {
await redis.delete(key);
},
},
});

@varundeva commented on GitHub (Aug 15, 2025): > make sure to also ref [this](https://github.com/redis/node-redis/issues/1000) for more info Im using redis package from npm, not node Redis `import { createClient, type RedisClientType } from "redis"; declare global { // eslint-disable-next-line no-var var _redisClient: RedisClientType | undefined; var _redisConnected: boolean | undefined; } let redisClient: RedisClientType; let isConnected = false; async function connectRedis() { if (!process.env.REDIS_URL) { throw new Error("REDIS_URL environment variable not set"); } if (!global._redisClient) { global._redisClient = createClient({ url: process.env.REDIS_URL, }); global._redisClient.on("error", (err) => console.error("Redis Client Error:", err), ); global._redisClient.on("connect", () => console.log("Redis client connected"), ); global._redisClient.on("end", () => console.log("Redis client disconnected"), ); global._redisConnected = false; } redisClient = global._redisClient; isConnected = global._redisConnected ?? false; if (!isConnected) { await redisClient.connect(); isConnected = true; global._redisConnected = true; console.log("Redis connected successfully"); } return redisClient; } export const redis = { get: async (key: string): Promise<string | null> => { const client = await connectRedis(); return client.get(key); }, set: async (key: string, value: string, ttl?: number): Promise<void> => { const client = await connectRedis(); if (ttl) { await client.set(key, value, { EX: ttl }); } else { await client.set(key, value); } }, delete: async (key: string): Promise<void> => { const client = await connectRedis(); await client.del(key); }, quit: async (): Promise<void> => { if (redisClient && isConnected) { await redisClient.quit(); isConnected = false; global._redisConnected = false; } }, keys: async (pattern: string): Promise<string[]> => { const client = await connectRedis(); return client.keys(pattern); }, exists: async (key: string): Promise<boolean> => { const client = await connectRedis(); return (await client.exists(key)) === 1; }, incr: async (key: string): Promise<number> => { const client = await connectRedis(); return client.incr(key); }, expire: async (key: string, seconds: number): Promise<boolean> => { const client = await connectRedis(); return (await client.expire(key, seconds)) === 1; }, }; export default redis;` auth.ts code is here `import { betterAuth } from "better-auth"; import dotenv from "dotenv"; dotenv.config(); import { MongoClient } from "mongodb"; import { mongodbAdapter } from "better-auth/adapters/mongodb"; import { nextCookies } from "better-auth/next-js"; import { admin } from "better-auth/plugins"; import redis from "@/lib/redis"; const client = new MongoClient( (process.env.MONGODB_URI as string) || "mongodb://localhost:27017", ); const db = client.db(); export const auth = betterAuth({ database: mongodbAdapter(db), socialProviders: { google: { prompt: "select_account", clientId: process.env.GOOGLE_CLIENT_ID as string, clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, }, linkedin: { clientId: process.env.LINKEDIN_CLIENT_ID as string, clientSecret: process.env.LINKEDIN_CLIENT_SECRET as string, }, tiktok: { clientId: process.env.TIKTOK_CLIENT_ID as string, clientSecret: process.env.TIKTOK_CLIENT_SECRET as string, clientKey: process.env.TIKTOK_CLIENT_KEY as string, }, facebook: { clientId: process.env.FACEBOOK_CLIENT_ID as string, clientSecret: process.env.FACEBOOK_CLIENT_SECRET as string, }, spotify: { clientId: process.env.SPOTIFY_CLIENT_ID as string, clientSecret: process.env.SPOTIFY_CLIENT_SECRET as string, }, }, account: { accountLinking: { enabled: true, trustedProviders: ["google", "linkedin", "facebook", "tiktok", "spotify"], }, }, plugins: [nextCookies(), admin()], secondaryStorage: { get: async (key) => { const value = await redis.get(key); return value ? value : null; }, set: async (key, value, ttl) => { if (ttl) await redis.set(key, value, ttl); else await redis.set(key, value); }, delete: async (key) => { await redis.delete(key); }, }, });
Author
Owner

@varundeva commented on GitHub (Aug 16, 2025):

Hey, I have debugged and found that it does not have a TTL
Here is a screenshot
while setting to Redis, a few don't have TTL,
Mainly, I can see that the keys are api endpoint names,
My Redis function to set is working fine,

Check screenshots below @Kinfe123

Image Image Image Image
@varundeva commented on GitHub (Aug 16, 2025): Hey, I have debugged and found that it does not have a TTL Here is a screenshot while setting to Redis, a few don't have TTL, Mainly, I can see that the keys are api endpoint names, My Redis function to set is working fine, Check screenshots below @Kinfe123 <img width="1244" height="485" alt="Image" src="https://github.com/user-attachments/assets/8eef22d6-f32b-483d-b869-311728783b2a" /> <img width="1150" height="813" alt="Image" src="https://github.com/user-attachments/assets/39f3302f-f627-47a4-ad56-8dd24e48c5ba" /> <img width="1232" height="604" alt="Image" src="https://github.com/user-attachments/assets/8086a82c-0350-48a2-b683-316d9e193a27" /> <img width="830" height="678" alt="Image" src="https://github.com/user-attachments/assets/97d0940f-eafc-4e8e-bd52-a9e3237cba8b" />
Author
Owner

@dvanmali commented on GitHub (Aug 19, 2025):

@ping-maxwell i've had this fixed

@dvanmali commented on GitHub (Aug 19, 2025): @ping-maxwell i've had this fixed
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1718