Relationships
#921 Auth hot path: 7 serial Atlas round trips per API-key request; session cookie cache dies after 5 min; session/apikey lookups unindexed
Opened by keeb · 7/2/2026· Shipped 7/2/2026
From the 2026-07-02 full-stack performance audit. Prod topology: app on DO k8s, Mongo on Atlas — ~50-80ms per round trip, so serial round-trip count dominates. Trivial authenticated endpoints average 700-800ms; this issue accounts for it.
CLI/API-key requests pay 7 serial Atlas round trips before doing any work
A trivial authenticated CLI GET (e.g. /api/whoami) pays, in order:
- A guaranteed-miss
findSession. BetterAuth's bearer plugin takes any dot-lessAuthorization: Bearervalue — includingswamp_...API keys — signs it itself and runsgetSession→internalAdapter.findSessionagainst Mongo. It can never match. Triggered atroutes/_middleware.ts:191before the API-key branch runs. auth.api.verifyApiKey→findOneonapikey.key(routes/_middleware.ts:246).- An awaited bookkeeping WRITE on every request: verifyApiKey decrements
remaining/ bumpsupdatedAtsynchronously becausedeferUpdatesis not set in the apiKey plugin options (lib/auth.ts:575-583). better-auth 1.4.18 supportsdeferUpdates: true(moves it to background). operativeRepo.findById(routes/_middleware.ts:250-253). 5-7. Inside whoami:listUserCollectivesWithRolesruns member find → organization find → operative_profile find, strictly serial (lib/infrastructure/better-auth-collective-queries.ts:219-271).
7 × 50-80ms ≈ 420-560ms — matches the observed latency floor.
Fixes:
- Reorder the middleware: when the header starts with
Bearer swamp_, skipgetSessionentirely (it can never be a session token). - Set
deferUpdates: trueon the apiKey plugin. - Add a small in-process TTL cache (30-60s, keyed by key hash) mapping API key → synthetic session. API keys are long-lived; steady state becomes ~0-1 round trips.
- Collapse
listUserCollectivesWithRolesto one aggregation:$match {userId}→$lookup organization→$lookup organization_profile. Supporting indexes already exist (better-auth-collective-queries.ts:426-439). - Same 2-serial-reads pattern in the collective-token branch (
routes/_middleware.ts:214-224):findByHashthenfindById—$lookupjoin or fold into the same cache.
Web session cookie cache permanently dies 5 minutes after sign-in
session.cookieCache IS enabled (lib/auth.ts:364-369, maxAge 300s) and a cache hit costs 0 DB. But the middleware calls auth.api.getSession({ headers }) without returnHeaders: true (routes/_middleware.ts:191), so when the cache cookie expires and BetterAuth re-issues it via Set-Cookie, we discard it. Result: every authenticated web request after minute 5 of a session pays a findSession round trip that the cache was configured to eliminate.
Fix: call getSession({ headers, returnHeaders: true }) and append the returned Set-Cookie header(s) to the outgoing response. Restores the designed steady state of 1 session read per user per 5 minutes.
No index on session.token or apikey.key
BetterAuth's Mongo adapter creates no indexes; migrations 001-011 don't cover these. Every session lookup (findOne({token})) and API-key lookup (findOne({key})) is a collscan — currently masked by small collections, becomes a second latency cliff on the growth curve.
Fix: migration: session.createIndex({token: 1}, {unique: true}), apikey.createIndex({key: 1}). Verify against prod Atlas first in case they were created manually.
Expected impact
CLI/API requests: 7 serial RTs → ~1 warm (5-7x on every CLI command). Web: removes 50-80ms from every authenticated page/API request after minute 5.
Verified clean (no action): in-memory rate limiter, fire-and-forget track() with 500ms timeout + circuit breaker, shared Mongo client with maxPoolSize 50, tracedRepo/withSpan overhead.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.