[GH-ISSUE #3573] API Key refillnternal and refillAmount ignored #26974

Closed
opened 2026-04-17 17:44:40 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @eaoliver on GitHub (Jul 23, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3573

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

The refillInterval and refillAmount parameters of the createApiKey handling don't seem to be respected. Consider the following configuration:

auth.api.createApiKey(
{
  ...
  // Enable rate limit throttling
  rateLimitEnabled: true,
  rateLimitTimeWindow: 300_00, // 30 seconds
  rateLimitMax: 30, // Max requests during rateLimitTimeWindow
  remaining: null, // There is no cap on usage.
  // Refill the API key one request per second
  refillAmount: 1,
  refillInterval: 1000, 
   ...
})

Current vs. Expected behavior

My intent is to allow a mean rate of 1 request per second, so the refillAmount and refillInterval are set accordingly. However, the behaviour is that the 30 tokens are consumed, and the verifyApiKey returns RATE_LIMITED . I then have to wait 30 seconds for the rate limit to be reset.

With a refill rate of 1 request / second, I would expect to never be rate limited if I'm only making API requests at 1 request / second.

What version of Better Auth are you using?

1.3.3

Provide environment information

Node: 20

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

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
});

Additional context

I have tested against the latest release.

Originally created by @eaoliver on GitHub (Jul 23, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3573 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce The `refillInterval` and `refillAmount` parameters of the createApiKey handling don't seem to be respected. Consider the following configuration: ``` auth.api.createApiKey( { ... // Enable rate limit throttling rateLimitEnabled: true, rateLimitTimeWindow: 300_00, // 30 seconds rateLimitMax: 30, // Max requests during rateLimitTimeWindow remaining: null, // There is no cap on usage. // Refill the API key one request per second refillAmount: 1, refillInterval: 1000, ... }) ``` ### Current vs. Expected behavior My intent is to allow a mean rate of 1 request per second, so the `refillAmount` and `refillInterval` are set accordingly. However, the behaviour is that the 30 tokens are consumed, and the `verifyApiKey` returns `RATE_LIMITED` . I then have to wait 30 seconds for the rate limit to be reset. **With a refill rate of 1 request / second, I would expect to never be rate limited if I'm only making API requests at 1 request / second.** ### What version of Better Auth are you using? 1.3.3 ### Provide environment information ```bash Node: 20 ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context I have tested against the latest release.
GiteaMirror added the lockedbug labels 2026-04-17 17:44:40 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Jul 23, 2025):

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

You're correct—refillInterval and refillAmount are not currently enforced as a leaky-bucket or steady refill mechanism in Better Auth v1.3.3. The actual rate limiting logic only uses rateLimitTimeWindow and rateLimitMax, so after you hit the max, you have to wait for the window to reset, regardless of the refill settings. The refill logic in the code is not integrated with the main rate limiter and is implemented using days as the interval, which makes it ineffective for sub-day intervals like 1 second. This matches what you're seeing: the config options exist, but they're not wired up to provide the behavior you expect (see code details).

As a workaround, you can try to approximate your desired rate by tuning rateLimitTimeWindow and rateLimitMax, but true token-bucket style refilling isn't supported yet. The documentation also doesn't mention refillInterval or refillAmount as part of the main rate limiting mechanism (see docs). This is likely a known limitation or an incomplete feature in the current version.

To reply, just mention @dosu.


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

<!-- gh-comment-id:3111325798 --> @dosubot[bot] commented on GitHub (Jul 23, 2025): <!-- Greeting --> Hi @eaoliver! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> You're correct—refillInterval and refillAmount are not currently enforced as a leaky-bucket or steady refill mechanism in Better Auth v1.3.3. The actual rate limiting logic only uses rateLimitTimeWindow and rateLimitMax, so after you hit the max, you have to wait for the window to reset, regardless of the refill settings. The refill logic in the code is not integrated with the main rate limiter and is implemented using days as the interval, which makes it ineffective for sub-day intervals like 1 second. This matches what you're seeing: the config options exist, but they're not wired up to provide the behavior you expect ([see code details](https://github.com/better-auth/better-auth/blob/e11c51e210cbc9c4b7177e13a9c8efd2985c856f/packages/better-auth/src/plugins/api-key/rate-limit.ts)). As a workaround, you can try to approximate your desired rate by tuning rateLimitTimeWindow and rateLimitMax, but true token-bucket style refilling isn't supported yet. The documentation also doesn't mention refillInterval or refillAmount as part of the main rate limiting mechanism ([see docs](https://better-auth.com/docs/concepts/rate-limit)). This is likely a known limitation or an incomplete feature in the current version. <!-- 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/3f05d6fb-0e8a-4703-8011-8a6d0bb027f1?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/3f05d6fb-0e8a-4703-8011-8a6d0bb027f1?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/3f05d6fb-0e8a-4703-8011-8a6d0bb027f1?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/3f05d6fb-0e8a-4703-8011-8a6d0bb027f1?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/3f05d6fb-0e8a-4703-8011-8a6d0bb027f1?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/3f05d6fb-0e8a-4703-8011-8a6d0bb027f1?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/3f05d6fb-0e8a-4703-8011-8a6d0bb027f1?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/3573)
Author
Owner

@ping-maxwell commented on GitHub (Jul 24, 2025):

Can you send your auth config properly? The one in your post doesn't even include the api key plugin or database.

<!-- gh-comment-id:3111839748 --> @ping-maxwell commented on GitHub (Jul 24, 2025): Can you send your auth config properly? The one in your post doesn't even include the api key plugin or database.
Author
Owner

@Adityakk9031 commented on GitHub (Jul 24, 2025):

@dosu tell me logic for rate limit is hidden or open

<!-- gh-comment-id:3111925095 --> @Adityakk9031 commented on GitHub (Jul 24, 2025): @dosu tell me logic for rate limit is hidden or open
Author
Owner

@dosubot[bot] commented on GitHub (Jul 24, 2025):

The rate limit logic in Better Auth is fully open and implemented in the codebase. You can find it in packages/better-auth/src/plugins/api-key/rate-limit.ts for API key rate limiting and packages/better-auth/src/api/rate-limiter/index.ts for general route/IP-based rate limiting. The logic uses rateLimitTimeWindow and rateLimitMax, but refillInterval and refillAmount are not enforced—they're just stored in the model and not used in the actual rate limiting checks. Nothing is hidden or obfuscated; it's all readable TypeScript code you can review or modify as needed.
See code example

To reply, just mention @dosu.


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

<!-- gh-comment-id:3111930670 --> @dosubot[bot] commented on GitHub (Jul 24, 2025): <!-- Answer --> The rate limit logic in Better Auth is fully open and implemented in the codebase. You can find it in packages/better-auth/src/plugins/api-key/rate-limit.ts for API key rate limiting and packages/better-auth/src/api/rate-limiter/index.ts for general route/IP-based rate limiting. The logic uses rateLimitTimeWindow and rateLimitMax, but refillInterval and refillAmount are not enforced—they're just stored in the model and not used in the actual rate limiting checks. Nothing is hidden or obfuscated; it's all readable TypeScript code you can review or modify as needed. [See code example](https://github.com/better-auth/better-auth/blob/c83b23c6ba7494d5997a06250d48d1777a2fabcf/packages/better-auth/src/plugins/api-key/rate-limit.ts) <!-- 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/e842454e-6b96-4db7-a5a3-d6712c3268af?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/e842454e-6b96-4db7-a5a3-d6712c3268af?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/e842454e-6b96-4db7-a5a3-d6712c3268af?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/e842454e-6b96-4db7-a5a3-d6712c3268af?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/e842454e-6b96-4db7-a5a3-d6712c3268af?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/e842454e-6b96-4db7-a5a3-d6712c3268af?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/e842454e-6b96-4db7-a5a3-d6712c3268af?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/3573)
Author
Owner

@eaoliver commented on GitHub (Jul 24, 2025):

Can you send your auth config properly? The one in your post doesn't even include the api key plugin or database.

Yes, my config file is as follows:

export const auth = betterAuth({
  advanced: {
    cookiePrefix: COOKIE_PREFIX,
    database: {
      generateId: false,
    },
  },
  database: drizzleAdapter(db, {
    provider: "pg",
    schema: {
      users: users,
      accounts: accounts,
      sessions: sessions,
      verifications: verifications,
      two_factors: twoFactors,
      apikeys: apikeys,
    },
    usePlural: true,
  }),
  emailAndPassword: {
    enabled: true,
    autoSignIn: true,
    requireEmailVerification: false,
    resetPasswordTokenExpiresIn: 3600,
    revokeSessionsOnPasswordReset: true,
    sendResetPassword: async (
      data: {user: User; url: string; token: string},
      request?: Request,
    ) => {
      // await sendEmail({
      //     to: user.email,
      //     subject: 'Reset your password',
      //     text: `Click the link to reset your password: ${url}`
      // })
    },
  },
  // Configure email: https://www.better-auth.com/docs/concepts/email
  emailVerification: {
    autoSignInAfterVerification: true,
    expiresIn: 3600,
    onEmailVerification: async (user: User, request?: Request) => {
      // Your custom logic here, e.g., grant access to premium features
      console.log(`${user.email} has been successfully verified!`);
    },
    sendVerificationEmail: async (
      data: {user: User; url: string; token: string},
      request?: Request,
    ) => {
      console.log("send verification email to", data.user.email);
      // await sendEmail({
      //     to: user.email,
      //     subject: 'Verify your email address',
      //     text: `Click the link to verify your email: ${url}`
      // })
    },
    sendOnSignUp: true,
  },
  plugins: [
    apiKey(),
    twoFactor(),

    // Last element of plugin array, per:
    // https://www.better-auth.com/docs/integrations/next
    // NOTE: this plugin cannot be used in conjuction with the initial DB
    // setup, which does not have access to nextjs headers.
    //    nextCookies(),
  ],
  session: {
    cookieCache: {
      enabled: true,
      maxAge: 900, // 15 minutes
    },
    expiresIn: 86_400 * 365, // 1 year
    updateAge: 86_400 * 7, // 1 week (every 1 week the session expiration is updated)
  },
  user: {
    changeEmail: {
      enabled: false,
    },
    deleteUser: {
      enabled: false,
      deleteTokenExpiresIn: 3600,
    },
  },
});
<!-- gh-comment-id:3113445246 --> @eaoliver commented on GitHub (Jul 24, 2025): > Can you send your auth config properly? The one in your post doesn't even include the api key plugin or database. Yes, my config file is as follows: ``` export const auth = betterAuth({ advanced: { cookiePrefix: COOKIE_PREFIX, database: { generateId: false, }, }, database: drizzleAdapter(db, { provider: "pg", schema: { users: users, accounts: accounts, sessions: sessions, verifications: verifications, two_factors: twoFactors, apikeys: apikeys, }, usePlural: true, }), emailAndPassword: { enabled: true, autoSignIn: true, requireEmailVerification: false, resetPasswordTokenExpiresIn: 3600, revokeSessionsOnPasswordReset: true, sendResetPassword: async ( data: {user: User; url: string; token: string}, request?: Request, ) => { // await sendEmail({ // to: user.email, // subject: 'Reset your password', // text: `Click the link to reset your password: ${url}` // }) }, }, // Configure email: https://www.better-auth.com/docs/concepts/email emailVerification: { autoSignInAfterVerification: true, expiresIn: 3600, onEmailVerification: async (user: User, request?: Request) => { // Your custom logic here, e.g., grant access to premium features console.log(`${user.email} has been successfully verified!`); }, sendVerificationEmail: async ( data: {user: User; url: string; token: string}, request?: Request, ) => { console.log("send verification email to", data.user.email); // await sendEmail({ // to: user.email, // subject: 'Verify your email address', // text: `Click the link to verify your email: ${url}` // }) }, sendOnSignUp: true, }, plugins: [ apiKey(), twoFactor(), // Last element of plugin array, per: // https://www.better-auth.com/docs/integrations/next // NOTE: this plugin cannot be used in conjuction with the initial DB // setup, which does not have access to nextjs headers. // nextCookies(), ], session: { cookieCache: { enabled: true, maxAge: 900, // 15 minutes }, expiresIn: 86_400 * 365, // 1 year updateAge: 86_400 * 7, // 1 week (every 1 week the session expiration is updated) }, user: { changeEmail: { enabled: false, }, deleteUser: { enabled: false, deleteTokenExpiresIn: 3600, }, }, }); ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#26974