Skip to content

Database rate limiting returns a huge X-Retry-After on Postgres because pg returns lastRequest as a string #11519

Description

@SisyphusMD
  • Yes, this is suited for github

Reproduction

  1. Use the built-in Kysely adapter with a pg Pool on PostgreSQL, and rateLimit: { enabled: true, storage: 'database' }, with lastRequest stored as bigint (the type the CLI generates).
  2. Send requests to a rate-limited endpoint (for example /sign-in/email with a custom rule of max: 4, window: 60) until one is blocked.
  3. Look at the 429 response's X-Retry-After header.

Current vs. Expected behavior

Current: the header is a huge number, for example 179080721351639. Blocking itself works.

Expected: the seconds left in the window, at most window (e.g. 60).

Cause: node-postgres returns int8 columns as strings by default. readRow in api/rate-limiter/index.ts converts lastRequest only when it is a BigInt:

if (typeof data?.lastRequest === "bigint") {
  data.lastRequest = Number(data.lastRequest);
}

so a string gets through. The window checks use subtraction (now - data.lastRequest), which coerces the string, so they're fine. But getRetryAfter adds: lastRequest + windowInMs - now concatenates "1727000000000" + 60000 before subtracting, which gives the huge value. Converting any non-number (Number(data.lastRequest) whenever it is not already a number) fixes it. As a workaround I'm setting a types.getTypeParser on the pool that parses INT8 as a number.

What version of Better Auth are you using?

1.7.4 (the same code is in 1.7.7 and on main)

System info

Node 26, pg 8.23.0, PostgreSQL 18, built-in Kysely adapter with a pg Pool.

Which area(s) are affected?

Backend

Auth config

betterAuth({
  database: new Pool({ /* connection */ }),
  rateLimit: {
    enabled: true,
    storage: 'database',
    modelName: 'rate_limit',
    fields: { lastRequest: 'last_request' },
    window: 60,
    max: 100,
    customRules: { '/sign-in/email': { window: 60, max: 4 } },
  },
});

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions