Rate Limiting Concurrency Control#
Better Auth enforces rate limits under concurrent load using optimistic concurrency control — no application-layer locks, no queuing. The mechanism centers on the incrementOne database primitive, whose WHERE clause acts as both a row selector and an invariant guard evaluated inside a single atomic SQL write. If the guard no longer holds when the write lands, the operation returns null and the caller retries.
This pattern is used in two locations:
| Context | Entry point |
|---|---|
| Core (non-API-key) rate limiter | rate-limiter/index.ts |
| API key quota & rate limiting | verify-api-key.ts |
The incrementOne Primitive#
incrementOne applies field = field + delta to a single row only while the WHERE clause still matches. The where array accepts comparison operators (gt, lt, lte, eq), so range invariants like remaining > 0 or count < max are evaluated inside the same atomic SQL statement — two concurrent callers cannot both observe count < max and both increment past the limit. Returns the updated row, or null if the guard matched nothing.
Adapter fallback: incrementOne is optional on CustomAdapter but always present on the factory-wrapped DBAdapter. Adapters without a native implementation receive a transaction-based fallback that wraps findOne + updateMany, re-applying the original where predicates as a compare-and-swap guard, with up to five retries before throwing on contention .
Core Rate Limiter (rate-limiter/index.ts)#
The createDatabaseStorageWrapper function drives two guarded incrementOne calls per request:
-
Window reset — when
timeSinceLastRequest >= windowInMs, anincrementOnewithset: { count: 1, lastRequest: now }is guarded onlastRequest <= data.lastRequest. This ensures only one concurrent caller resets the window counter; others fall through to a recursive retry. -
In-window increment — within a window,
count += 1is guarded onlastRequest > windowStart AND count < max. Anullreturn triggers a re-read and recursiveconsume()call.
Fresh key creation uses db.create instead of incrementOne. If the insert fails (unique constraint from a concurrent opener), the code re-reads and retries — ensuring one request opens the window and the rest are counted .
bigint normalization: After reading a row, readRow coerces data.lastRequest from JavaScript bigint to Number if needed — PostgreSQL bigint columns can surface this type at runtime .
API Key Rate Limiting (verify-api-key.ts)#
Quota: consumeRemaining#
consumeRemaining handles keys with a finite remaining counter via two sequential guarded writes:
- Refill CAS — if a refill interval has elapsed,
incrementOneguarded onid AND lastRefillAt = <observed value>resetsremainingtorefillAmount - 1. Only one concurrent caller wins; others fall through to the plain decrement . - Guarded decrement —
incrementOnewith{ remaining: -1 }guarded onremaining > 0. Anullreturn means quota is exhausted →USAGE_EXCEEDED.
Rate Limit: consumeRateLimit#
consumeRateLimit first calls evaluateRateLimit to produce one of five decisions (skip, start, reset, increment, deny), then maps each to a guarded write:
| Decision | Guard condition | Action |
|---|---|---|
skip | — | stamp lastRequest only (or no-op) |
start | lastRequest IS NULL | set requestCount = 1 |
reset | lastRequest <= windowStart | set requestCount = 1 |
increment | lastRequest > windowStart AND requestCount < max | requestCount += 1 |
deny | — | throw TOO_MANY_REQUESTS immediately |
Recursive retry: when any incrementOne returns null (guard raced), the code re-fetches the row and calls consumeRateLimit recursively . This handles both window-roll races and burst races.
Secondary Storage Caveat#
When API keys use storage: "secondary-storage" without fallbackToDatabase, quota and rate-limit consumption goes through claimUsageInSecondaryStorage — a non-atomic read-modify-write. Two concurrent requests can both read the same snapshot and both succeed. A FIXME comment tracks backing this with an atomic SecondaryStorage.increment.
The core rate limiter's secondary-storage path uses SecondaryStorage.increment (which creates the counter with a fixed TTL on first use) and is atomic by design .
For strict quota and rate-limit enforcement, use storage: "database" or storage: "secondary-storage" with fallbackToDatabase: true.