[GH-ISSUE #4023] Double rate-limit increment when authenticating via API key (verification + session lookup) #18431

Closed
opened 2026-04-15 16:53:02 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @vojtech-cerveny on GitHub (Aug 15, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/4023

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

//nextjs 15.3.4
"use server";

export async function checkUserAuthOrThrowError(request: Request) {
  const apiKey = request.headers.get("X-API-Key");
  if (!apiKey) return errorResponse("Unauthorized", ApiErrorCode.UNAUTHORIZED);

  const apiKeyResponse = await apikeyService.server.verifyApiKey(apiKey);
  if (!apiKeyResponse.valid) {
    return errorResponse(apiKeyResponse.error?.message ?? "Invalid API KEY", ApiErrorCode.OPERATION_FAILED);
  }

  // (Counter observed here)

  const session = await userService.server.getSession(
    new Headers({ "x-api-key": apiKey }),
  );

  // (Counter observed here)

  if (!session) return errorResponse("Unauthorized", ApiErrorCode.UNAUTHORIZED);
  return session.user;
}

Current vs. Expected behavior

Description
When a client calls our endpoint with X-API-Key, we:
1. validate the API key via apikeyService.server.verifyApiKey(apiKey) and
2. fetch the user/session via userService.server.getSession(...).

This approach is needed for download correct user information.

Both steps increment the API key’s request counter, resulting in two rate-limit increments for a single request.

Impact
• Users burn rate-limit quota twice per request.
• Makes per-minute/hour quotas unfairly restrictive.
• Harder to reason about where increments happen.

Actual behavior

  • 2 rate-limit increment - one for validation of api-key, second fetching session/user info.
    Expected behavior
    • Exactly one rate-limit increment per incoming request that passes through auth.

Possible fixes:

  1. Combine verify + user load in one call - Introduce validateAPIKey - it would return Session as Better-auth-ui has (link)
  2. Add optional options: { skipRateLimit?: boolean } to getSession (or verifyApiKey) and set it to true when it’s the second internal call in the same request.

What version of Better Auth are you using?

1.3.5

System info

System:
    OS: macOS 15.5
    CPU: (8) arm64 Apple M2
    Memory: 104.73 MB / 24.00 GB
    Shell: 5.9 - /bin/zsh
  Browsers:
    Brave Browser: 134.1.76.81
    Chrome: 139.0.7258.67
    Chrome Canary: 141.0.7356.0
    Safari: 18.5

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

Backend

Auth config (if applicable)

export const auth = betterAuth({
  database: prismaAdapter(prisma, {
    provider: "postgresql",
  }),
  emailAndPassword: {
    enabled: true,
  },
  session: {
    expiresIn: SESSION.EXPIRES_IN,
    updateAge: SESSION.UPDATE_AGE,
  },
  telemetry: { enabled: false },
  plugins: [
    apiKey({
      rateLimit: {
        enabled: true,
        timeWindow: RATE_LIMIT.TIME_WINDOW,
        maxRequests: RATE_LIMIT.MAX_REQUESTS,
      },
    }),
    adminPlugin({
      ac,
      roles: {
        admin,
        user,
      },
    }),
  ],
});

Additional context

No response

Originally created by @vojtech-cerveny on GitHub (Aug 15, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/4023 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce ```typescript //nextjs 15.3.4 "use server"; export async function checkUserAuthOrThrowError(request: Request) { const apiKey = request.headers.get("X-API-Key"); if (!apiKey) return errorResponse("Unauthorized", ApiErrorCode.UNAUTHORIZED); const apiKeyResponse = await apikeyService.server.verifyApiKey(apiKey); if (!apiKeyResponse.valid) { return errorResponse(apiKeyResponse.error?.message ?? "Invalid API KEY", ApiErrorCode.OPERATION_FAILED); } // (Counter observed here) const session = await userService.server.getSession( new Headers({ "x-api-key": apiKey }), ); // (Counter observed here) if (!session) return errorResponse("Unauthorized", ApiErrorCode.UNAUTHORIZED); return session.user; } ``` ### Current vs. Expected behavior **Description** When a client calls our endpoint with X-API-Key, we: 1. validate the API key via apikeyService.server.verifyApiKey(apiKey) and 2. fetch the user/session via userService.server.getSession(...). This approach is needed for download correct user information. Both steps increment the API key’s request counter, resulting in two rate-limit increments for a single request. **Impact** • Users burn rate-limit quota twice per request. • Makes per-minute/hour quotas unfairly restrictive. • Harder to reason about where increments happen. **Actual behavior** * 2 rate-limit increment - one for validation of api-key, second fetching session/user info. **Expected behavior** • Exactly one rate-limit increment per incoming request that passes through auth. **Possible fixes:** 1. Combine verify + user load in one call - Introduce `validateAPIKey` - it would return `Session` as Better-auth-ui has ([link](https://better-auth-ui.com/advanced/api-keys#server-side-validation)) 2. Add optional options: { skipRateLimit?: boolean } to getSession (or verifyApiKey) and set it to true when it’s the second internal call in the same request. ### What version of Better Auth are you using? 1.3.5 ### System info ```bash System: OS: macOS 15.5 CPU: (8) arm64 Apple M2 Memory: 104.73 MB / 24.00 GB Shell: 5.9 - /bin/zsh Browsers: Brave Browser: 134.1.76.81 Chrome: 139.0.7258.67 Chrome Canary: 141.0.7356.0 Safari: 18.5 ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript export const auth = betterAuth({ database: prismaAdapter(prisma, { provider: "postgresql", }), emailAndPassword: { enabled: true, }, session: { expiresIn: SESSION.EXPIRES_IN, updateAge: SESSION.UPDATE_AGE, }, telemetry: { enabled: false }, plugins: [ apiKey({ rateLimit: { enabled: true, timeWindow: RATE_LIMIT.TIME_WINDOW, maxRequests: RATE_LIMIT.MAX_REQUESTS, }, }), adminPlugin({ ac, roles: { admin, user, }, }), ], }); ``` ### Additional context _No response_
GiteaMirror added the lockedbug labels 2026-04-15 16:53:02 -05:00
Author
Owner

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

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

The double rate-limit increment happens because both verifyApiKey and getSession are separate endpoints/methods, and each one triggers the rate-limiting logic independently. This is by design in Better Auth v1.3.5: every call that passes through the rate-limiter middleware increments the counter, and there’s no built-in way to combine these calls or skip the increment for internal calls in the current public API or plugin options (source).

Your proposed solutions are both feasible directions, but neither is available out of the box:

  • There’s no combined method in the public API that both verifies the API key and retrieves the session in a single call (source).
  • There’s no skipRateLimit option for verifyApiKey or getSession (source).

A practical workaround is to structure your API so that you only call the session endpoint (which, when using the API key plugin, already performs both verification and session retrieval in its middleware for the /get-session route). This way, you avoid the double increment by not calling verifyApiKey separately. If you need both validation and session info, rely on the session endpoint’s response and error handling.

If you need more granular control (like a skipRateLimit option or a combined method), it would require a code change or feature addition to Better Auth. This could be a good candidate for a future enhancement.

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:3190795797 --> @dosubot[bot] commented on GitHub (Aug 15, 2025): <!-- Greeting --> Hi @vojtech-cerveny! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The double rate-limit increment happens because both verifyApiKey and getSession are separate endpoints/methods, and each one triggers the rate-limiting logic independently. This is by design in Better Auth v1.3.5: every call that passes through the rate-limiter middleware increments the counter, and there’s no built-in way to combine these calls or skip the increment for internal calls in the current public API or plugin options ([source](https://github.com/better-auth/better-auth/blob/dbaae0ab2bc39de87dd4719e37f48e4b94ea2262/packages/better-auth/src/api/rate-limiter/index.ts)). Your proposed solutions are both feasible directions, but neither is available out of the box: - There’s no combined method in the public API that both verifies the API key and retrieves the session in a single call ([source](https://github.com/better-auth/better-auth/blob/dbaae0ab2bc39de87dd4719e37f48e4b94ea2262/packages/better-auth/src/plugins/api-key/routes/index.ts)). - There’s no skipRateLimit option for verifyApiKey or getSession ([source](https://github.com/better-auth/better-auth/blob/dbaae0ab2bc39de87dd4719e37f48e4b94ea2262/packages/better-auth/src/plugins/api-key/index.ts)). A practical workaround is to structure your API so that you only call the session endpoint (which, when using the API key plugin, already performs both verification and session retrieval in its middleware for the /get-session route). This way, you avoid the double increment by not calling verifyApiKey separately. If you need both validation and session info, rely on the session endpoint’s response and error handling. If you need more granular control (like a skipRateLimit option or a combined method), it would require a code change or feature addition to Better Auth. This could be a good candidate for a future enhancement. <!-- 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/9bf68f49-5e0c-4471-9a5f-554cbd96ef47?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/9bf68f49-5e0c-4471-9a5f-554cbd96ef47?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/9bf68f49-5e0c-4471-9a5f-554cbd96ef47?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/9bf68f49-5e0c-4471-9a5f-554cbd96ef47?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/9bf68f49-5e0c-4471-9a5f-554cbd96ef47?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/9bf68f49-5e0c-4471-9a5f-554cbd96ef47?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/9bf68f49-5e0c-4471-9a5f-554cbd96ef47?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/4023)
Author
Owner

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

You don't need to verify first, since getSession will verify the api key for you if it's provided in the header

<!-- gh-comment-id:3192097742 --> @Bekacru commented on GitHub (Aug 15, 2025): You don't need to verify first, since `getSession` will verify the api key for you if it's provided in the header
Author
Owner

@vojtech-cerveny commented on GitHub (Aug 19, 2025):

Thanks @Bekacru . Yeah, I figured out, that function verifyApiKey is useless there.

This is what works properly. Thanks!

//nextjs 15.3.4
"use server";

export async function checkUserAuthOrThrowError(request: Request) {
  const apiKey = request.headers.get("X-API-Key");
  if (!apiKey) return errorResponse("Unauthorized", ApiErrorCode.UNAUTHORIZED);

  const session = await userService.server.getSession(
    new Headers({ "x-api-key": apiKey }),
  );
  if (!session) return errorResponse("Unauthorized", ApiErrorCode.UNAUTHORIZED);
  return session.user;
}
<!-- gh-comment-id:3200362347 --> @vojtech-cerveny commented on GitHub (Aug 19, 2025): Thanks @Bekacru . Yeah, I figured out, that function verifyApiKey is useless there. This is what works properly. Thanks! ```typescript //nextjs 15.3.4 "use server"; export async function checkUserAuthOrThrowError(request: Request) { const apiKey = request.headers.get("X-API-Key"); if (!apiKey) return errorResponse("Unauthorized", ApiErrorCode.UNAUTHORIZED); const session = await userService.server.getSession( new Headers({ "x-api-key": apiKey }), ); if (!session) return errorResponse("Unauthorized", ApiErrorCode.UNAUTHORIZED); return session.user; } ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#18431