Skip to content

API & Navigation Performance ​

The symptom ​

In production, moving between in-app routes (calendar → posts → single post → media) left the page blank for ~0.8 s, and some backend calls looked like they were hanging. /api/auth/get-session was the most visible one because route middleware awaited it before every navigation.

What the measurements showed ​

CallResult
GET /api/auth/get-session (logged in)~187 ms p50, one 647 ms spike
GET /api/v1/inbox/count392 ms for a 35-byte body
GET /api/v1/business/active410 ms for a 564-byte body
GET /api/v1/assets (returned 500)342 ms
SPA navigation (URL change)725–820 ms

The decisive clue: endpoints that failed in the handler still took ~340 ms. The time was spent before the handler.

Root cause 1 — three session lookups per API request ​

Every authenticated /api/v1/* request resolved the same session three times:

  1. packages/auth/server/middleware/001.auth.ts → checkUserIsLogin().
  2. packages/auth/server/middleware/002.evlog-identify.ts → evlog's createAuthMiddleware() called auth.api.getSession() again.
  3. The route handler itself → checkUserIsLogin(event) again (265 call sites).

Each lookup is ~100–125 ms, so ~300–350 ms of pure auth overhead preceded every API call, and one slow query was amplified three times.

Fix — one promise per request ​

packages/auth/server/utils/request-session.ts memoizes the session (and the converted request headers) on event.context.authRequest. The cache stores the promise, so concurrent awaiters share one in-flight lookup. useAuthApi() delegates to it, which fixes all 265 handler call sites without touching routes. 002.evlog-identify.ts uses the same resolver and identifyUser() from evlog/better-auth.

The cache lives on the H3 event, so nothing outlives the request and concurrent requests cannot see each other's session.

Root cause 2 — navigation blocked on a session round trip ​

packages/auth/app/middleware/01.auth.global.ts awaited fetchSession() on every route change, even though the session was already hydrated from the SSR payload. A hard load paid for the session twice (SSR + after hydration).

Fix — trust hydrated state, revalidate cheaply ​

  • UseUser() gained sessionLoaded and sessionCheckedAt state.
  • Middleware fetches only when the session was never loaded or is older than 5 minutes.
  • fetchSession() is single-flight: the promise lives on the Nuxt app (per request on SSR, per tab on the client), replacing the old watch + 15 s timeout helper that could stall navigation.
  • The client fetch has a 5 s AbortSignal.timeout; on failure the cached state wins and the next signal/navigation retries.

packages/auth/lib/auth.ts had session.cookieCache commented out, so every session read hit the database. It is now enabled with maxAge: 60: reads are validated from the signed cookie, and a ban/role/revocation takes at most 60 s to propagate. Better Auth rewrites the cookie on every session mutation.

Also fixed ​

  • Post list requests were sent three times per /app/posts visit (PostFiltersBar emitted both filterChange and refresh, plus the page setup fetch). The component now emits one event, and usePostManager.getPosts collapses identical concurrent requests through a per-app in-flight map.
  • /api/v1/social-accounts/health fired twice per load under different callOnce keys and returned ~400 KB. The layout is now the only caller, the header badge reads shared state, and ?summary=1 returns only { id, platform, accountName, health } + the summary.
  • Better Auth logging is warn in production instead of debug.

How to verify ​

  1. Log in, open DevTools → Network, and clear it.
  2. Navigate calendar → posts → media → calendar.
  3. Each navigation should fire one /api/auth/get-session at most (and none within the 5-minute window), and /api/v1/posts should appear once.
  4. Server-side, every /api/v1/* request should show a single auth: span on its evlog wide event.

Trade-offs ​

  • cookieCache.maxAge: 60 means a revoked session, ban, or role change can take up to a minute to be enforced. Lower it if that window is unacceptable.
  • A session is validated per request but not per navigation; an expired session surfaces as a 401 from the next API call and the next revalidation (≤5 min).

Released under the MIT License.