ADR-0004. OTP state in Redis (TTL-based), not in PostgreSQL
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-02-12 |
| Deciders | identity-team |
| Supersedes | — |
Context
- OTP flows have transient state with strict TTL semantics:
- Hashed code (10-minute TTL)
- Attempt counter (lockout after 5 attempts)
- Cooldown after each send (60s)
- Daily quota counter
- Storing in PostgreSQL means: TTL via cron-like cleanup, locking on counter increments, table bloat from millions of expired rows.
Decision
All OTP runtime state lives in Redis with key namespacing:
| Key | TTL | Purpose |
|---|---|---|
{ns}:otp:{identifier} | 5–15 min | Hashed code |
{ns}:lock:{identifier} | 10–15 min | Account lockout |
{ns}:session:{token} | 1–24 h | Verified-session token |
{ns}:cooldown:{identifier} | 60s | Resend cooldown |
{ns}:daily:{identifier} | 24 h | Daily quota |
Namespaces: verify-email, verify-phone, forgot-password, phone-auth, add-phone, add-email.
Codes are hashed with Bun.password before storage.
Consequences
| Pros | Cons |
|---|---|
| TTL-based expiry — no cleanup job needed | OTP flows hard-fail if Redis is down |
| Atomic INCR for counters | State is ephemeral — lost on Redis flush |
| Sub-millisecond reads | Code hashing adds CPU on every verify |
| No row-bloat in PostgreSQL | Audit trail of OTP attempts not persisted |
Failure mode
If Redis is unavailable:
- New OTP requests return
503 Service Unavailable - Existing JWT verification still works (cache is at sister side)
- Previously-issued OTP tokens still verify if cached client-side, but verification fails server-side
Alternatives Considered
| Option | Pros | Cons | Why rejected |
|---|---|---|---|
| PostgreSQL with cron cleanup | Audit trail | Lock contention on counters; cleanup job latency; bloat | Wrong tradeoff |
| In-memory only (per-instance) | Fastest | Doesn't survive restarts; doesn't scale to N pods | Ops fragility |
| Hybrid (DB write + Redis cache) | Audit + speed | Double-write coordination | Complexity not worth it |
References
BaseOTPBasedMFAService(abstract base)EmailOtpService,PhoneOtpService(concrete)BindingKeys.APPLICATION_REDIS_BULLMQ