Reproduction
- 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).
- 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.
- 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 } },
},
});
Reproduction
pgPoolon PostgreSQL, andrateLimit: { enabled: true, storage: 'database' }, withlastRequeststored asbigint(the type the CLI generates)./sign-in/emailwith a custom rule ofmax: 4, window: 60) until one is blocked.X-Retry-Afterheader.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
int8columns as strings by default.readRowinapi/rate-limiter/index.tsconvertslastRequestonly when it is aBigInt:so a string gets through. The window checks use subtraction (
now - data.lastRequest), which coerces the string, so they're fine. ButgetRetryAfteradds:lastRequest + windowInMs - nowconcatenates"1727000000000" + 60000before 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 atypes.getTypeParseron the pool that parsesINT8as 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,
pg8.23.0, PostgreSQL 18, built-in Kysely adapter with apgPool.Which area(s) are affected?
Backend
Auth config