secondaryStorage always gets default TTL #2163

Closed
opened 2026-03-13 09:31:17 -05:00 by GiteaMirror · 7 comments
Owner

Originally created by @grrowl on GitHub (Oct 21, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Related issues:

Using this config:

{
  appName: "app",
  baseURL: env.BETTER_AUTH_URL || "http://localhost:5173",
  secret: env.BETTER_AUTH_SECRET || "temp-secret-key-for-development-only",
  session: {
    cookieCache: {
      enabled: true,
      maxAge: 5 * 60, // 5 minutes cache
    },
  },
  socialProviders: {
    google: {
      clientId: env.GOOGLE_CLIENT_ID || "",
      clientSecret: env.GOOGLE_CLIENT_SECRET || "",
    },
  },
  emailAndPassword: {
    enabled: true,
  },
  rateLimit: {
    enabled: true, // better-auth
    window: 60, // Minimum KV TTL is 60s
    max: 100, // reqs/window
  },
  plugins: [
    organization({
      // ...
    }),
    admin({
      adminRoles: ["admin"],
      defaultRole: "user",
    }),
  ],
  secondaryStorage: {
    get: async (key: string): Promise<any> => {
      return null;
    },
    set: async (key: string, value: any, ttl?: number): Promise<void> => {
      console.log("setting with ttl:", key, value, ttl);
    },
    delete: async (key: string): Promise<void> => {},
  },
}

I see the following log, no matter the value for rateLimit.window:

setting with ttl: 127.0.0.1/sign-in/social {"key":"127.0.0.1/sign-in/social","count":1,"lastRequest":1761041293089} 10

What is preventing the window making it through? Here's the default fallback we're getting instead:

https://github.com/better-auth/better-auth/pull/4961/files#diff-266a45270a21e4b11dcd53ed439c47158c823a5305a589055fa2307f5a543d44R102

onRequestRateLimit is fairly complex and not covered by tests:

https://github.com/better-auth/better-auth/blob/b347dfbedd4d039af464bca77fffed8a1e84cf91/packages/better-auth/src/api/rate-limiter/index.ts#L124

Current vs. Expected behavior

rateLimit.window should be passed as ttl to secondaryStorage, but instead it gets the default value 10

What version of Better Auth are you using?

1.3.28

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:30 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6020",
    "release": "24.6.0",
    "cpuCount": 12,
    "cpuModel": "Apple M2 Max",
    "totalMemory": "32.00 GB",
    "freeMemory": "0.20 GB"
  },
  "node": {
    "version": "v24.1.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.3.0"
  },
  "frameworks": [
    {
      "name": "react",
      "version": "^19.2.0"
    }
  ],
  "databases": [
    {
      "name": "drizzle",
      "version": "^0.44.6"
    }
  ],
  "betterAuth": {
    "version": "^1.3.28",
    "config": {
      "appName": "BrainWaves",
      "baseURL": "http://localhost:5173",
      "secret": "[REDACTED]",
      "session": {
        "cookieCache": {
          "enabled": true,
          "maxAge": 300
        },
        "storeSessionInDatabase": true
      },
      "socialProviders": {
        "google": {
          "clientId": "",
          "clientSecret": ""
        }
      },
      "emailAndPassword": {
        "enabled": true
      },
      "rateLimit": {
        "enabled": true,
        "window": 60000,
        "max": 100
      },
      "plugins": [
        {
          "name": "cloudflare",
          "config": {
            "id": "cloudflare",
            "schema": {
              "session": {
                "fields": {
                  "timezone": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "city": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "country": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "region": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "regionCode": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "colo": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "latitude": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "longitude": {
                    "type": "string",
                    "required": false,
                    "input": false
                  }
                }
              }
            },
            "endpoints": {}
          }
        },
        {
          "name": "organization",
          "config": {
            "id": "organization",
            "endpoints": {},
            "schema": {
              "organization": {
                "fields": {
                  "name": {
                    "type": "string",
                    "required": true,
                    "sortable": true
                  },
                  "slug": {
                    "type": "string",
                    "required": true,
                    "unique": true,
                    "sortable": true
                  },
                  "logo": {
                    "type": "string",
                    "required": false
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  },
                  "metadata": {
                    "type": "string",
                    "required": false
                  }
                }
              },
              "member": {
                "fields": {
                  "organizationId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "organization",
                      "field": "id"
                    }
                  },
                  "userId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "user",
                      "field": "id"
                    }
                  },
                  "role": {
                    "type": "string",
                    "required": true,
                    "sortable": true,
                    "defaultValue": "member"
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  }
                }
              },
              "invitation": {
                "fields": {
                  "organizationId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "organization",
                      "field": "id"
                    }
                  },
                  "email": {
                    "type": "string",
                    "required": true,
                    "sortable": true
                  },
                  "role": {
                    "type": "string",
                    "required": false,
                    "sortable": true
                  },
                  "status": {
                    "type": "string",
                    "required": true,
                    "sortable": true,
                    "defaultValue": "pending"
                  },
                  "expiresAt": {
                    "type": "date",
                    "required": true
                  },
                  "inviterId": {
                    "type": "string",
                    "references": {
                      "model": "user",
                      "field": "id"
                    },
                    "required": true
                  }
                }
              },
              "session": {
                "fields": {
                  "activeOrganizationId": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            },
            "$Infer": {
              "Organization": {},
              "Invitation": {},
              "Member": {},
              "Team": {},
              "TeamMember": {},
              "ActiveOrganization": {}
            },
            "$ERROR_CODES": {
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION": "You are not allowed to create a new organization",
              "YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS": "You have reached the maximum number of organizations",
              "ORGANIZATION_ALREADY_EXISTS": "Organization already exists",
              "ORGANIZATION_NOT_FOUND": "Organization not found",
              "USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION": "User is not a member of the organization",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION": "You are not allowed to update this organization",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION": "You are not allowed to delete this organization",
              "NO_ACTIVE_ORGANIZATION": "No active organization",
              "USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION": "User is already a member of this organization",
              "MEMBER_NOT_FOUND": "Member not found",
              "ROLE_NOT_FOUND": "Role not found",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM": "You are not allowed to create a new team",
              "TEAM_ALREADY_EXISTS": "Team already exists",
              "TEAM_NOT_FOUND": "Team not found",
              "YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER": "You cannot leave the organization as the only owner",
              "YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER": "You cannot leave the organization without an owner",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER": "You are not allowed to delete this member",
              "YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION": "You are not allowed to invite users to this organization",
              "USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION": "User is already invited to this organization",
              "INVITATION_NOT_FOUND": "Invitation not found",
              "YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION": "You are not the recipient of the invitation",
              "EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION": "Email verification required before accepting or rejecting invitation",
              "YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION": "You are not allowed to cancel this invitation",
              "INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION": "Inviter is no longer a member of the organization",
              "YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE": "You are not allowed to invite a user with this role",
              "FAILED_TO_RETRIEVE_INVITATION": "Failed to retrieve invitation",
              "YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS": "You have reached the maximum number of teams",
              "UNABLE_TO_REMOVE_LAST_TEAM": "Unable to remove last team",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER": "You are not allowed to update this member",
              "ORGANIZATION_MEMBERSHIP_LIMIT_REACHED": "Organization membership limit reached",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION": "You are not allowed to create teams in this organization",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION": "You are not allowed to delete teams in this organization",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM": "You are not allowed to update this team",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM": "You are not allowed to delete this team",
              "INVITATION_LIMIT_REACHED": "Invitation limit reached",
              "TEAM_MEMBER_LIMIT_REACHED": "Team member limit reached",
              "USER_IS_NOT_A_MEMBER_OF_THE_TEAM": "User is not a member of the team",
              "YOU_CAN_NOT_ACCESS_THE_MEMBERS_OF_THIS_TEAM": "You are not allowed to list the members of this team",
              "YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM": "You do not have an active team",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER": "You are not allowed to create a new member",
              "YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER": "You are not allowed to remove a team member",
              "YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION": "You are not allowed to access this organization as an owner",
              "YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION": "You are not a member of this organization",
              "MISSING_AC_INSTANCE": "Dynamic Access Control requires a pre-defined ac instance on the server auth plugin. Read server logs for more information",
              "YOU_MUST_BE_IN_AN_ORGANIZATION_TO_CREATE_A_ROLE": "You must be in an organization to create a role",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE": "You are not allowed to create a role",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE": "You are not allowed to update a role",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE": "You are not allowed to delete a role",
              "YOU_ARE_NOT_ALLOWED_TO_READ_A_ROLE": "You are not allowed to read a role",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE": "You are not allowed to list a role",
              "YOU_ARE_NOT_ALLOWED_TO_GET_A_ROLE": "You are not allowed to get a role",
              "TOO_MANY_ROLES": "This organization has too many roles",
              "INVALID_RESOURCE": "The provided permission includes an invalid resource",
              "ROLE_NAME_IS_ALREADY_TAKEN": "That role name is already taken",
              "CANNOT_DELETE_A_PRE_DEFINED_ROLE": "Cannot delete a pre-defined role"
            },
            "options": {
              "allowUserToCreateOrganization": true,
              "organizationLimit": 10
            }
          }
        },
        {
          "name": "admin",
          "config": {
            "id": "admin",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "$ERROR_CODES": {
              "FAILED_TO_CREATE_USER": "Failed to create user",
              "USER_ALREADY_EXISTS": "User already exists.",
              "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "User already exists. Use another email.",
              "YOU_CANNOT_BAN_YOURSELF": "You cannot ban yourself",
              "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "You are not allowed to create users",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "You are not allowed to list users",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "You are not allowed to list users sessions",
              "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "You are not allowed to ban users",
              "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "You are not allowed to impersonate users",
              "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "You are not allowed to revoke users sessions",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "You are not allowed to delete users",
              "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]",
              "BANNED_USER": "You have been banned from this application",
              "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "You are not allowed to get user",
              "NO_DATA_TO_UPDATE": "No data to update",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "You are not allowed to update users",
              "YOU_CANNOT_REMOVE_YOURSELF": "You cannot remove yourself"
            },
            "schema": {
              "user": {
                "fields": {
                  "role": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banned": {
                    "type": "boolean",
                    "defaultValue": false,
                    "required": false,
                    "input": false
                  },
                  "banReason": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banExpires": {
                    "type": "date",
                    "required": false,
                    "input": false
                  }
                }
              },
              "session": {
                "fields": {
                  "impersonatedBy": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            },
            "options": {
              "adminRoles": [
                "admin"
              ],
              "defaultRole": "user"
            }
          }
        }
      ],
      "advanced": {
        "ipAddress": {
          "ipAddressHeaders": [
            "cf-connecting-ip",
            "x-real-ip"
          ]
        }
      }
    }
  }
}

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

Backend

Auth config (if applicable)


Additional context

No response

Originally created by @grrowl on GitHub (Oct 21, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce Related issues: - https://github.com/better-auth/better-auth/pull/3836: adds ttl sessions list expiration - https://github.com/better-auth/better-auth/pull/4961: adds default `window` TTL of 10s Using this config: ``` { appName: "app", baseURL: env.BETTER_AUTH_URL || "http://localhost:5173", secret: env.BETTER_AUTH_SECRET || "temp-secret-key-for-development-only", session: { cookieCache: { enabled: true, maxAge: 5 * 60, // 5 minutes cache }, }, socialProviders: { google: { clientId: env.GOOGLE_CLIENT_ID || "", clientSecret: env.GOOGLE_CLIENT_SECRET || "", }, }, emailAndPassword: { enabled: true, }, rateLimit: { enabled: true, // better-auth window: 60, // Minimum KV TTL is 60s max: 100, // reqs/window }, plugins: [ organization({ // ... }), admin({ adminRoles: ["admin"], defaultRole: "user", }), ], secondaryStorage: { get: async (key: string): Promise<any> => { return null; }, set: async (key: string, value: any, ttl?: number): Promise<void> => { console.log("setting with ttl:", key, value, ttl); }, delete: async (key: string): Promise<void> => {}, }, } ``` I see the following log, no matter the value for `rateLimit.window`: ``` setting with ttl: 127.0.0.1/sign-in/social {"key":"127.0.0.1/sign-in/social","count":1,"lastRequest":1761041293089} 10 ``` What is preventing the `window` making it through? Here's the default fallback we're getting instead: https://github.com/better-auth/better-auth/pull/4961/files#diff-266a45270a21e4b11dcd53ed439c47158c823a5305a589055fa2307f5a543d44R102 `onRequestRateLimit` is fairly complex and not covered by tests: https://github.com/better-auth/better-auth/blob/b347dfbedd4d039af464bca77fffed8a1e84cf91/packages/better-auth/src/api/rate-limiter/index.ts#L124 ### Current vs. Expected behavior `rateLimit.window` should be passed as `ttl` to `secondaryStorage`, but instead it gets the default value `10` ### What version of Better Auth are you using? 1.3.28 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:30 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6020", "release": "24.6.0", "cpuCount": 12, "cpuModel": "Apple M2 Max", "totalMemory": "32.00 GB", "freeMemory": "0.20 GB" }, "node": { "version": "v24.1.0", "env": "development" }, "packageManager": { "name": "npm", "version": "11.3.0" }, "frameworks": [ { "name": "react", "version": "^19.2.0" } ], "databases": [ { "name": "drizzle", "version": "^0.44.6" } ], "betterAuth": { "version": "^1.3.28", "config": { "appName": "BrainWaves", "baseURL": "http://localhost:5173", "secret": "[REDACTED]", "session": { "cookieCache": { "enabled": true, "maxAge": 300 }, "storeSessionInDatabase": true }, "socialProviders": { "google": { "clientId": "", "clientSecret": "" } }, "emailAndPassword": { "enabled": true }, "rateLimit": { "enabled": true, "window": 60000, "max": 100 }, "plugins": [ { "name": "cloudflare", "config": { "id": "cloudflare", "schema": { "session": { "fields": { "timezone": { "type": "string", "required": false, "input": false }, "city": { "type": "string", "required": false, "input": false }, "country": { "type": "string", "required": false, "input": false }, "region": { "type": "string", "required": false, "input": false }, "regionCode": { "type": "string", "required": false, "input": false }, "colo": { "type": "string", "required": false, "input": false }, "latitude": { "type": "string", "required": false, "input": false }, "longitude": { "type": "string", "required": false, "input": false } } } }, "endpoints": {} } }, { "name": "organization", "config": { "id": "organization", "endpoints": {}, "schema": { "organization": { "fields": { "name": { "type": "string", "required": true, "sortable": true }, "slug": { "type": "string", "required": true, "unique": true, "sortable": true }, "logo": { "type": "string", "required": false }, "createdAt": { "type": "date", "required": true }, "metadata": { "type": "string", "required": false } } }, "member": { "fields": { "organizationId": { "type": "string", "required": true, "references": { "model": "organization", "field": "id" } }, "userId": { "type": "string", "required": true, "references": { "model": "user", "field": "id" } }, "role": { "type": "string", "required": true, "sortable": true, "defaultValue": "member" }, "createdAt": { "type": "date", "required": true } } }, "invitation": { "fields": { "organizationId": { "type": "string", "required": true, "references": { "model": "organization", "field": "id" } }, "email": { "type": "string", "required": true, "sortable": true }, "role": { "type": "string", "required": false, "sortable": true }, "status": { "type": "string", "required": true, "sortable": true, "defaultValue": "pending" }, "expiresAt": { "type": "date", "required": true }, "inviterId": { "type": "string", "references": { "model": "user", "field": "id" }, "required": true } } }, "session": { "fields": { "activeOrganizationId": { "type": "string", "required": false } } } }, "$Infer": { "Organization": {}, "Invitation": {}, "Member": {}, "Team": {}, "TeamMember": {}, "ActiveOrganization": {} }, "$ERROR_CODES": { "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION": "You are not allowed to create a new organization", "YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS": "You have reached the maximum number of organizations", "ORGANIZATION_ALREADY_EXISTS": "Organization already exists", "ORGANIZATION_NOT_FOUND": "Organization not found", "USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION": "User is not a member of the organization", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION": "You are not allowed to update this organization", "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION": "You are not allowed to delete this organization", "NO_ACTIVE_ORGANIZATION": "No active organization", "USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION": "User is already a member of this organization", "MEMBER_NOT_FOUND": "Member not found", "ROLE_NOT_FOUND": "Role not found", "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM": "You are not allowed to create a new team", "TEAM_ALREADY_EXISTS": "Team already exists", "TEAM_NOT_FOUND": "Team not found", "YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER": "You cannot leave the organization as the only owner", "YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER": "You cannot leave the organization without an owner", "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER": "You are not allowed to delete this member", "YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION": "You are not allowed to invite users to this organization", "USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION": "User is already invited to this organization", "INVITATION_NOT_FOUND": "Invitation not found", "YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION": "You are not the recipient of the invitation", "EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION": "Email verification required before accepting or rejecting invitation", "YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION": "You are not allowed to cancel this invitation", "INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION": "Inviter is no longer a member of the organization", "YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE": "You are not allowed to invite a user with this role", "FAILED_TO_RETRIEVE_INVITATION": "Failed to retrieve invitation", "YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS": "You have reached the maximum number of teams", "UNABLE_TO_REMOVE_LAST_TEAM": "Unable to remove last team", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER": "You are not allowed to update this member", "ORGANIZATION_MEMBERSHIP_LIMIT_REACHED": "Organization membership limit reached", "YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION": "You are not allowed to create teams in this organization", "YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION": "You are not allowed to delete teams in this organization", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM": "You are not allowed to update this team", "YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM": "You are not allowed to delete this team", "INVITATION_LIMIT_REACHED": "Invitation limit reached", "TEAM_MEMBER_LIMIT_REACHED": "Team member limit reached", "USER_IS_NOT_A_MEMBER_OF_THE_TEAM": "User is not a member of the team", "YOU_CAN_NOT_ACCESS_THE_MEMBERS_OF_THIS_TEAM": "You are not allowed to list the members of this team", "YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM": "You do not have an active team", "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER": "You are not allowed to create a new member", "YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER": "You are not allowed to remove a team member", "YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION": "You are not allowed to access this organization as an owner", "YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION": "You are not a member of this organization", "MISSING_AC_INSTANCE": "Dynamic Access Control requires a pre-defined ac instance on the server auth plugin. Read server logs for more information", "YOU_MUST_BE_IN_AN_ORGANIZATION_TO_CREATE_A_ROLE": "You must be in an organization to create a role", "YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE": "You are not allowed to create a role", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE": "You are not allowed to update a role", "YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE": "You are not allowed to delete a role", "YOU_ARE_NOT_ALLOWED_TO_READ_A_ROLE": "You are not allowed to read a role", "YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE": "You are not allowed to list a role", "YOU_ARE_NOT_ALLOWED_TO_GET_A_ROLE": "You are not allowed to get a role", "TOO_MANY_ROLES": "This organization has too many roles", "INVALID_RESOURCE": "The provided permission includes an invalid resource", "ROLE_NAME_IS_ALREADY_TAKEN": "That role name is already taken", "CANNOT_DELETE_A_PRE_DEFINED_ROLE": "Cannot delete a pre-defined role" }, "options": { "allowUserToCreateOrganization": true, "organizationLimit": 10 } } }, { "name": "admin", "config": { "id": "admin", "hooks": { "after": [ {} ] }, "endpoints": {}, "$ERROR_CODES": { "FAILED_TO_CREATE_USER": "Failed to create user", "USER_ALREADY_EXISTS": "User already exists.", "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "User already exists. Use another email.", "YOU_CANNOT_BAN_YOURSELF": "You cannot ban yourself", "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role", "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "You are not allowed to create users", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "You are not allowed to list users", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "You are not allowed to list users sessions", "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "You are not allowed to ban users", "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "You are not allowed to impersonate users", "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "You are not allowed to revoke users sessions", "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "You are not allowed to delete users", "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]", "BANNED_USER": "You have been banned from this application", "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "You are not allowed to get user", "NO_DATA_TO_UPDATE": "No data to update", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "You are not allowed to update users", "YOU_CANNOT_REMOVE_YOURSELF": "You cannot remove yourself" }, "schema": { "user": { "fields": { "role": { "type": "string", "required": false, "input": false }, "banned": { "type": "boolean", "defaultValue": false, "required": false, "input": false }, "banReason": { "type": "string", "required": false, "input": false }, "banExpires": { "type": "date", "required": false, "input": false } } }, "session": { "fields": { "impersonatedBy": { "type": "string", "required": false } } } }, "options": { "adminRoles": [ "admin" ], "defaultRole": "user" } } } ], "advanced": { "ipAddress": { "ipAddressHeaders": [ "cf-connecting-ip", "x-real-ip" ] } } } } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript ``` ### Additional context _No response_
GiteaMirror added the bug label 2026-03-13 09:31:17 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Oct 21, 2025):

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

The reason secondaryStorage.set() always receives a TTL of 10 seconds is due to how the TTL is determined and passed in the rate limiter logic. In the code, the TTL is set using rateLimitSettings?.window ?? ctx.options.rateLimit?.window ?? 10, but the value for window can be overridden by special rules for certain paths (like /sign-in/social), which default to 10 seconds. If your request matches one of these paths, the TTL will always be 10, regardless of your config. Also, if window is set in milliseconds (e.g., 60000), it may not be converted to seconds, causing unexpected TTL values. The relevant code path is in getRateLimitStorage, and the logic for special rules is in getDefaultSpecialRules.

To ensure your configured rateLimit.window is used, make sure your path does not match a special rule, and set window in seconds. If you need more control, you can use a customStorage implementation to explicitly set the TTL to your desired value when calling secondaryStorage.set (see 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 (Oct 21, 2025): <!-- Greeting --> Hi @grrowl! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The reason `secondaryStorage.set()` always receives a TTL of 10 seconds is due to how the TTL is determined and passed in the rate limiter logic. In the code, the TTL is set using `rateLimitSettings?.window ?? ctx.options.rateLimit?.window ?? 10`, but the value for `window` can be overridden by special rules for certain paths (like `/sign-in/social`), which default to 10 seconds. If your request matches one of these paths, the TTL will always be 10, regardless of your config. Also, if `window` is set in milliseconds (e.g., `60000`), it may not be converted to seconds, causing unexpected TTL values. The relevant code path is in [`getRateLimitStorage`](https://github.com/better-auth/better-auth/blob/fa6ee9d68214c6a51e7253b53f957407b116b0c4/packages/better-auth/src/api/rate-limiter/index.ts), and the logic for special rules is in `getDefaultSpecialRules`. To ensure your configured `rateLimit.window` is used, make sure your path does not match a special rule, and set `window` in seconds. If you need more control, you can use a `customStorage` implementation to explicitly set the TTL to your desired value when calling `secondaryStorage.set` ([see workaround](https://github.com/better-auth/better-auth/issues/4472)). <!-- 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/d5dffbf1-db46-4343-b320-055dc6380389?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/d5dffbf1-db46-4343-b320-055dc6380389?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/d5dffbf1-db46-4343-b320-055dc6380389?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/d5dffbf1-db46-4343-b320-055dc6380389?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/d5dffbf1-db46-4343-b320-055dc6380389?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/d5dffbf1-db46-4343-b320-055dc6380389?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/d5dffbf1-db46-4343-b320-055dc6380389?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/5452)
Author
Owner

@grrowl commented on GitHub (Oct 21, 2025):

@dosu ok homie, i am certainly trying to sign in. the value of 10 is not compatible with my KV provider (Cloudflare KV, min value 60s). how do i override this behaviour? give me a rateLimiter config that fixes the issue.

@grrowl commented on GitHub (Oct 21, 2025): @dosu ok homie, i am certainly trying to sign in. the value of `10` is not compatible with my KV provider (Cloudflare KV, min value 60s). how do i override this behaviour? give me a `rateLimiter` config that fixes the issue.
Author
Owner

@dosubot[bot] commented on GitHub (Oct 21, 2025):

You can override the default 10s TTL for sign-in rate limiting by using the customStorage option in your rateLimit config. This lets you explicitly set the TTL to at least 60 seconds (Cloudflare KV's minimum) when storing rate limit keys. Here’s a copy-paste config example:

rateLimit: {
  enabled: true,
  window: 60, // seconds
  max: 100,
  customStorage: {
    get: async (key: string) => {
      const data = await secondaryStorage.get(key);
      return data ? JSON.parse(data) : undefined;
    },
    set: async (key: string, value: any) => {
      // Set TTL to at least 60s for Cloudflare KV
      await secondaryStorage.set(key, JSON.stringify(value), 60);
    },
    delete: async (key: string) => {
      await secondaryStorage.delete(key);
    },
  },
}

This ensures all rate limit keys use a TTL compatible with Cloudflare KV. You can also use window instead of 60 if you want the TTL to match your config. This workaround is necessary because Better Auth hardcodes a 10s window for sign-in routes, which can't be changed via config alone—customStorage gives you full control over TTL handling. Reference

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 (Oct 21, 2025): <!-- Answer --> You can override the default 10s TTL for sign-in rate limiting by using the `customStorage` option in your `rateLimit` config. This lets you explicitly set the TTL to at least 60 seconds (Cloudflare KV's minimum) when storing rate limit keys. Here’s a copy-paste config example: ```typescript rateLimit: { enabled: true, window: 60, // seconds max: 100, customStorage: { get: async (key: string) => { const data = await secondaryStorage.get(key); return data ? JSON.parse(data) : undefined; }, set: async (key: string, value: any) => { // Set TTL to at least 60s for Cloudflare KV await secondaryStorage.set(key, JSON.stringify(value), 60); }, delete: async (key: string) => { await secondaryStorage.delete(key); }, }, } ``` This ensures all rate limit keys use a TTL compatible with Cloudflare KV. You can also use `window` instead of `60` if you want the TTL to match your config. This workaround is necessary because Better Auth hardcodes a 10s window for sign-in routes, which can't be changed via config alone—customStorage gives you full control over TTL handling. [Reference](https://github.com/better-auth/better-auth/issues/4472) <!-- 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/1d587b33-fb05-461e-a3bd-823e74f534f8?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/1d587b33-fb05-461e-a3bd-823e74f534f8?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/1d587b33-fb05-461e-a3bd-823e74f534f8?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/1d587b33-fb05-461e-a3bd-823e74f534f8?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/1d587b33-fb05-461e-a3bd-823e74f534f8?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/1d587b33-fb05-461e-a3bd-823e74f534f8?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/1d587b33-fb05-461e-a3bd-823e74f534f8?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/5452)
Author
Owner

@dvanmali commented on GitHub (Oct 21, 2025):

There is a default rate limit for specific endpoints of max 3 every 10 seconds (window) such as /sign-in/social. To change these values use rateLimit.customRules.

@dvanmali commented on GitHub (Oct 21, 2025): There is a default rate limit for specific endpoints of max 3 every 10 seconds (window) such as `/sign-in/social`. To change these values use `rateLimit.customRules`.
Author
Owner

@grrowl commented on GitHub (Oct 23, 2025):

@dvanmali thanks mate. This config doesn't work, any chance you can point me in the right direction?

    rateLimit: {
      enabled: true,
      window: 60, // Minimum KV TTL is 60s
      max: 100, // reqs/window
      customRules: {
        // https://github.com/better-auth/better-auth/issues/5452
        '*': {
          window: 60,
          max: 100,
        }
    },

Have raised an issue on better-auth-cloudflare too.

@grrowl commented on GitHub (Oct 23, 2025): @dvanmali thanks mate. This config doesn't work, any chance you can point me in the right direction? ``` rateLimit: { enabled: true, window: 60, // Minimum KV TTL is 60s max: 100, // reqs/window customRules: { // https://github.com/better-auth/better-auth/issues/5452 '*': { window: 60, max: 100, } }, ``` Have raised an issue on `better-auth-cloudflare` too.
Author
Owner

@dvanmali commented on GitHub (Oct 23, 2025):

@grrowl I don't think '*' is a valid custom rule route, try /sign-in/social and /sign-in/email

@dvanmali commented on GitHub (Oct 23, 2025): @grrowl I don't think `'*'` is a valid custom rule route, try `/sign-in/social` and `/sign-in/email`
Author
Owner

@dosubot[bot] commented on GitHub (Jan 22, 2026):

Hi, @grrowl. I'm Dosu, and I'm helping the better-auth team manage their backlog and am marking this issue as stale.

Issue Summary:

  • You reported that the secondaryStorage set method uses a hardcoded 10-second TTL, ignoring your configured rateLimit.window (e.g., 60s), which conflicts with Cloudflare KV's minimum TTL.
  • This behavior is due to special path-based rules (like /sign-in/social) that enforce a 10-second TTL.
  • A customStorage implementation was suggested to explicitly override the TTL.
  • It was noted that default rate limits for certain endpoints can be changed via rateLimit.customRules, but using '*' as a wildcard does not work; exact paths must be specified.
  • The issue remains unresolved as you seek a proper configuration to fix the TTL mismatch.

Next Steps:

  • Please confirm if this issue is still relevant with the latest version of better-auth, and if so, you can keep the discussion open by commenting here.
  • Otherwise, I will automatically close this issue in 7 days.

Thank you for your understanding and contribution!

@dosubot[bot] commented on GitHub (Jan 22, 2026): Hi, @grrowl. I'm [Dosu](https://dosu.dev), and I'm helping the better-auth team manage their backlog and am marking this issue as stale. **Issue Summary:** - You reported that the secondaryStorage set method uses a hardcoded 10-second TTL, ignoring your configured rateLimit.window (e.g., 60s), which conflicts with Cloudflare KV's minimum TTL. - This behavior is due to special path-based rules (like /sign-in/social) that enforce a 10-second TTL. - A customStorage implementation was suggested to explicitly override the TTL. - It was noted that default rate limits for certain endpoints can be changed via rateLimit.customRules, but using '*' as a wildcard does not work; exact paths must be specified. - The issue remains unresolved as you seek a proper configuration to fix the TTL mismatch. **Next Steps:** - Please confirm if this issue is still relevant with the latest version of better-auth, and if so, you can keep the discussion open by commenting here. - Otherwise, I will automatically close this issue in 7 days. Thank you for your understanding and contribution!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2163