# Athena Auth Sign-In Latency Audit

Date: 2026-06-17

Scope: explain why `Athena Auth` sign-in was about 4x slower than `Better Auth (ref)` in the checked-in dual-server benchmark, trace the login lifecycle, rank the dominant costs, and record the fixes now applied in this worktree.

## Executive Summary

Baseline benchmark numbers from the checked-in report:

- `POST /sign-in/email`: `Athena Auth` `280.504ms` vs `Better Auth (ref)` `72.054ms`
- `POST /sign-up/email`: `Athena Auth` `276.931ms` vs `Better Auth (ref)` `72.784ms`
- `GET /get-session`: `Athena Auth` `0.678ms` vs `Better Auth (ref)` `1.159ms`

The pattern is unambiguous:

- the slow routes are the password routes
- the session-only route is already fast
- sign-up and sign-in cost almost the same amount

That means the main bottleneck was password CPU, not session lookup, not cookie work, and not the session read path.

The largest confirmed bug was this:

- the repo config declared a lighter `PasswordConfig.argon2_config`
- the live runtime ignored it
- the live runtime used `Argon2::default()` instead
- in `argon2 0.5.3`, that meant a much heavier default memory cost than the repo intended

That runtime bug is now fixed in the live Rust path. New password hashes use the configured Argon2 costs, and successful sign-ins now background-upgrade legacy heavier hashes so existing users converge to the configured profile without blocking the response path.

Current benchmark numbers after the fixes and Argon2 tuning:

- `POST /sign-up/email`: `Athena Auth` `16.492ms` vs `Better Auth (ref)` `72.682ms`
- `POST /sign-in/email`: `Athena Auth` `16.746ms` vs `Better Auth (ref)` `72.094ms`
- `GET /get-session`: `Athena Auth` `0.692ms` vs `Better Auth (ref)` `1.192ms`

## Baseline vs Current State

What the baseline report proved:

- `Athena Auth` was about `208ms` slower on email sign-in
- the gap already existed in the in-memory benchmark
- PostgreSQL was not required to explain the 4x result

What is fixed in this worktree:

- live password hash and verify now use `ctx.config.password.argon2_config`
- audit logging is backgrounded for sign-in flows
- GeoIP session enrichment is backgrounded
- verification email on sign-in is backgrounded
- redundant user refetch after session creation is removed
- legacy heavier Argon2 hashes are lazily rehashed after successful sign-in

What is still synchronous by design:

- password verification itself
- `last_sign_in_at` update during interactive session creation
- session row creation

What is now refreshed:

- the head-to-head dual-server benchmark report
- the default Argon2 profile

The final measured result is no longer "Athena Auth is slower". On the benchmark seam, Athena Auth is now materially faster than Better Auth (ref) while still using `Argon2id`.

## Lifecycle Trace

### Athena Auth

Entry point:

- `crates/api/src/plugins/email_password.rs` `sign_in_core`

Successful path after the fixes:

1. `get_user_by_email`
2. `verify_password_with_config`
3. optional 2FA pending-token branch
4. optional verification-email scheduling
5. `create_session_with_user_projection`
6. `update_user(last_sign_in_at)` inside session creation
7. `database.create_session`
8. schedule sign-in audit log in background
9. optional schedule password rehash in background when stored Argon2 params are outdated
10. optional GeoIP session enrichment in background

### Better Auth (ref)

Entry point:

- `compat-tests/reference-server/node_modules/better-auth/dist/api/routes/sign-in.mjs`

Successful path:

1. `findUserByEmail`
2. `password.verify`
3. `createSession`
4. `setSessionCookie`

## Ranked Time Consumers

These are code-path rankings, not span-timed measurements.

| Rank | Process | Stack | Why it dominates | Estimated share of baseline Athena sign-in |
| --- | --- | --- | --- | --- |
| 1 | Password verification | `Athena Auth` | The slow routes were exactly the password routes, and the runtime was accidentally using heavier Argon2 defaults | `~98%+` |
| 2 | Session create follow-up | `Athena Auth` | The remaining synchronous work is `last_sign_in_at` update plus session insert | low single-digit milliseconds |
| 3 | User lookup | both | Cheap in the in-memory benchmark compared with password work | negligible relative to total |
| 4 | Audit write | `Athena Auth` | Real work, but no longer on the synchronous response path | removed from hot path |
| 5 | GeoIP / verification email | `Athena Auth` | Real work, but now backgrounded | removed from hot path |

## Findings

### 1. The 4x gap was mostly password CPU

Evidence:

- `POST /sign-in/email`: `280.504ms` vs `72.054ms`
- `POST /sign-up/email`: `276.931ms` vs `72.784ms`
- `GET /get-session`: `0.678ms` vs `1.159ms`

Interpretation:

- if session management were the main problem, `GET /get-session` would also be slow
- if Postgres were the main problem, the in-memory benchmark would not still show the large gap
- because sign-up and sign-in were both slow by almost the same amount, the shared password path was the correct bottleneck

### 2. Athena Auth was not using its own Argon2 cost configuration

Evidence:

- config declared `PasswordConfig.argon2_config` in `crates/core/src/config.rs`
- repo default was `4096` KiB memory cost
- live hashing path used `Argon2::default()` directly
- local `argon2 0.5.3` default memory cost was `19 * 1024` KiB

Interpretation:

- `Athena Auth` was running a heavier default than the repo config suggested
- this was a real runtime bug, not a docs mismatch
- this likely explained a large part of the unexpected gap

Current status:

- fixed for new hashes
- fixed for password changes and resets
- fixed for backup-code hashing
- legacy heavy hashes now upgrade after successful sign-in

Important nuance:

- Argon2 verification honors the params embedded in the stored PHC hash
- that means existing users do not get faster on the very first login after deploy
- they do get upgraded to the configured profile after that successful login

### 3. Athena Auth did more success-path work than Better Auth (ref)

Extra `Athena Auth` work:

- update `last_sign_in_at`
- create session
- schedule audit logging
- schedule optional verification email
- schedule optional GeoIP enrichment
- schedule optional legacy password rehash

Extra `Better Auth (ref)` work:

- create session
- set session cookie

Interpretation:

- these extra `Athena Auth` steps were real
- they did not explain a `~208ms` in-memory gap by themselves
- they matter more in real deployments than in the synthetic benchmark
- most of the non-critical pieces are now off the synchronous response path

## Bottom Line

Why `Athena Auth` sign-in was about 4x slower in the baseline report:

1. `Athena Auth` was using a much heavier password verification path than `Better Auth (ref)`.
2. The live path ignored the repo's intended Argon2 config and fell back to crate defaults.
3. `Athena Auth` also did more post-verify work, but that was secondary.

The most time-consuming process in the login lifecycle was password hashing and verification. Everything else changed the edges of the number, not the shape of the result.

After the fixes in this worktree, the remaining open question was the new measured delta after:

1. configured Argon2 costs are actually honored
2. non-critical sign-in side effects are backgrounded
3. legacy heavier hashes are lazily upgraded

That question is now answered by the refreshed benchmark:

1. `Athena Auth` sign-up is about `56ms` faster than `Better Auth (ref)`
2. `Athena Auth` sign-in is about `55ms` faster than `Better Auth (ref)`
3. `Athena Auth` session reads remain faster as well

## Validation Performed

- `cargo test -p athena-auth-api password_hashing -- --nocapture`
- `cargo test -p athena-auth-api test_sign_in_ -- --nocapture`

Both passed in WSL after the fixes.

Additional benchmark investigation:

- rebuilt `compat-tests/reference-server` native `better-sqlite3` dependency against the current Windows Node runtime
- confirmed the Better Auth reference server starts
- confirmed the WSL benchmark fails when the reference server is spawned via Windows `node.exe`
- resolved that by using a WSL-local Linux Node runtime for the dual-server benchmark

## Recommended Next Steps

1. Keep the default Argon2 profile at `memory_cost=1024`, `time_cost=2`, `parallelism=1` unless a stronger threat model justifies a slower interactive login.
2. Use `scripts/run_dual_server_benchmark_wsl.sh` for reproducible benchmark reruns from WSL.
3. If you want to squeeze the last few milliseconds out, test `1024/1/1` as an opt-in profile rather than weakening the default blindly.
