Bug
When rateLimit.storage is set to "database", the adapter:create mutation throws "rateLimit key already exists" on the second request to any auth endpoint within the same rate limit window.
This blocks all auth flows (session checks, OTP sending, sign-in) once a rate limit entry exists.
Reproduction
- Configure better-auth with
rateLimit: { enabled: true, storage: "database" }
- Make any auth request (e.g.,
get-session)
- Make the same request again within the rate limit window
- The second request fails with:
[CONVEX M(adapter:create)] Uncaught Error: rateLimit key already exists
at checkUniqueFields (adapter-utils.ts:276:2)
at async handler (create-api.ts:86:8)
Root Cause
The better-auth core rate limiter (api/rate-limiter/index.mjs, createDatabaseStorageWrapper) has a set(key, value, _update) method:
- When
_update=false, it calls db.create() (insert)
- When
_update=true, it calls db.updateMany() (update)
Under certain conditions (race conditions, or when onResponseRateLimit runs with !data despite the key existing), _update is false even though a record with that key already exists. The Convex adapter's create handler runs checkUniqueFields, finds the duplicate key, and throws.
The error is caught and logged by better-auth core, but in the Convex adapter it surfaces as an uncaught mutation error that aborts the entire HTTP action.
Suggested Fix
The adapter's create handler for the rateLimit model could use upsert semantics instead of insert-or-throw. Since key is the unique field, the adapter could check for an existing entry and update it rather than throwing.
Alternatively, adding a try-catch around the checkUniqueFields call specifically for the rateLimit model would prevent the cascading failure.
Workaround
Set rateLimit: { enabled: false } or use storage: "memory" instead of "database".
Environment
@convex-dev/better-auth: 0.11.4
better-auth: latest (via better-auth/minimal)
- Convex deployment: EU West 1
Bug
When
rateLimit.storageis set to"database", theadapter:createmutation throws"rateLimit key already exists"on the second request to any auth endpoint within the same rate limit window.This blocks all auth flows (session checks, OTP sending, sign-in) once a rate limit entry exists.
Reproduction
rateLimit: { enabled: true, storage: "database" }get-session)Root Cause
The better-auth core rate limiter (
api/rate-limiter/index.mjs,createDatabaseStorageWrapper) has aset(key, value, _update)method:_update=false, it callsdb.create()(insert)_update=true, it callsdb.updateMany()(update)Under certain conditions (race conditions, or when
onResponseRateLimitruns with!datadespite the key existing),_updateisfalseeven though a record with thatkeyalready exists. The Convex adapter'screatehandler runscheckUniqueFields, finds the duplicatekey, and throws.The error is caught and logged by better-auth core, but in the Convex adapter it surfaces as an uncaught mutation error that aborts the entire HTTP action.
Suggested Fix
The adapter's
createhandler for therateLimitmodel could use upsert semantics instead of insert-or-throw. Sincekeyis the unique field, the adapter could check for an existing entry and update it rather than throwing.Alternatively, adding a try-catch around the
checkUniqueFieldscall specifically for therateLimitmodel would prevent the cascading failure.Workaround
Set
rateLimit: { enabled: false }or usestorage: "memory"instead of"database".Environment
@convex-dev/better-auth: 0.11.4better-auth: latest (viabetter-auth/minimal)