Fixing Auth Race Conditions in Production
The Bug That Only Happens in Production
Your app works perfectly in development. But in production, users randomly get logged out. Sound familiar?
Common Causes
1. **Token refresh timing**: Multiple requests try to refresh simultaneously
2. **Race between storage and API**: Token used before it's saved
3. **Stale closures**: Old token values captured in callbacks
Debugging Strategy
#
Step 1: Add Comprehensive Logging
```typescript
const refreshToken = async () => {
console.log('[Auth] Refresh started at:', Date.now())
console.log('[Auth] Current token expiry:', tokenExpiry)
// ... refresh logic
console.log('[Auth] Refresh completed at:', Date.now())
}
```
#
Step 2: Implement Token Refresh Lock
```typescript
let isRefreshing = false
let refreshSubscribers: ((token: string) => void)[] = []
const refreshTokenWithLock = async () => {
if (isRefreshing) {
return new Promise
refreshSubscribers.push(resolve)
})
}
isRefreshing = true
try {
const newToken = await refreshToken()
refreshSubscribers.forEach((cb) => cb(newToken))
refreshSubscribers = []
return newToken
} finally {
isRefreshing = false
}
}
```
#
Step 3: Use Atomic Storage Operations
```typescript
// Bad: Race condition possible
await AsyncStorage.setItem('accessToken', newAccessToken)
await AsyncStorage.setItem('refreshToken', newRefreshToken)
// Good: Atomic multi-set
await AsyncStorage.multiSet([
['accessToken', newAccessToken],
['refreshToken', newRefreshToken],
])
```
The Fix That Worked
After implementing these patterns in the Inbo app, auth-related crashes dropped by 99.7%.
Key changes:
1. Single refresh queue with promise deduplication
2. Atomic token storage with multiSet
3. Request retry with fresh token on 401
4. Proactive refresh 5 minutes before expiry