Skip to main content
By default theauth keeps rate-limit counters, DPoP proof replay records and the CIMD document cache in process memory. With one replica that is fine. With several, each replica enforces its own limits, and a DPoP proof replayed against a different replica is accepted. Config.Stores fixes that. It takes three small interfaces from the kv package, and any field you leave nil keeps the in-memory default for that piece.
What uses each one:
  • RateLimitByIP, RateLimitByEmail and the OAuth endpoint limits use RateLimiter.
  • DPoP proof jti replay protection uses ReplayCache. If the backend errors, the proof is rejected.
  • The CIMD document cache uses Cache. Documents read from the shared tier are validated again.
The client-auth Argon2id cache, the introspection cache and the mcpresource JWKS cache are still per process. They hold derived data, not security decisions.

Pick an adapter

Memory is the default and suits one process:
SQL reuses the database you already run. For Postgres through pgx:
For MySQL and SQLite pass sqlkv.MySQL or sqlkv.SQLite with the *sql.DB you gave the storage package. sqlkv.Schema(dialect, table) returns the DDL if you would rather run it through your own migration tool. Expired rows are pruned on a fraction of writes, so call store.Prune(ctx) from a timer if traffic is low. The limiter that kv.FromCache builds is a fixed window, which means a burst can reach twice the limit across a window boundary. Redis needs no driver dependency in theauth. The kv/redis package asks your client for one method:
Every operation is one Lua script, so it is atomic and costs one round trip. You can mix backends. A Redis limiter with the default replay cache is just kv.Stores{RateLimiter: ...}.

Writing your own backend

Implement any of the interfaces. kvtest.Run(t, factory) is a contract suite for kv.Cache implementations, and the factory receives a fake clock to hand to your adapter.

Behavior to know about

  • Rate-limit keys include a rule name and a per-middleware sequence number, so RateLimitByIP(5) on sign-in and RateLimitByIP(5) on sign-up keep separate budgets. Replicas share budgets as long as they mount routes in the same order, which is the normal case.
  • A limiter backend error fails open for sign-in middleware (an outage should not lock everyone out) and for the OAuth endpoint limits. The device verification page fails closed.
  • A shared limiter does not help if every request appears to come from your load balancer. Set TrustedProxies as well.
If you are choosing between backends for the main storage, see Storage backends.
Last modified on October 9, 2026