This error occurs when Supabase detects a refresh token being reused outside the allowed reuse interval, typically due to race conditions in SSR applications or network issues causing stale tokens to be retried.
Supabase Auth implements refresh token reuse detection as a security feature to prevent token theft. Each refresh token can only be used once, with a default 10-second reuse interval window to accommodate legitimate scenarios like server-side rendering where the same token may need to be used on both server and client. When a refresh token is used to obtain a new access token, it becomes invalid. If an attempt is made to reuse that same token outside the reuse interval, Supabase treats this as a potential security breach and terminates the entire session, revoking all associated refresh tokens. This mechanism protects against scenarios where a refresh token might have been stolen through leaked logs, compromised third-party servers, or man-in-the-middle attacks. While this can cause frustration during development, it's an important security feature that prevents unauthorized access to user accounts.
Ensure the authentication state change listener is registered early in your application lifecycle to properly manage token state:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(supabaseUrl, supabaseKey)
// Register listener at app initialization
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'TOKEN_REFRESHED') {
console.log('Token refreshed successfully')
// Update your app state with new session
}
if (event === 'SIGNED_OUT') {
// Handle logout, clear local state
}
})This ensures your application properly tracks when tokens are refreshed and prevents using stale tokens.
Implement error handling that catches refresh token errors and prompts for re-authentication:
async function handleTokenRefresh() {
try {
const { data, error } = await supabase.auth.refreshSession()
if (error) throw error
return data.session
} catch (error) {
if (error.message.includes('refresh_token_already_used')) {
// Clear stored session
await supabase.auth.signOut()
// Redirect to login or show re-authentication prompt
window.location.href = '/login?session_expired=true'
}
throw error
}
}This gracefully handles the error by prompting users to log in again rather than leaving them in a broken state.
If using Next.js or similar SSR frameworks, ensure you're not creating multiple Supabase clients that could trigger parallel refresh attempts:
// app/layout.tsx - Create client once at root
import { createServerClient } from '@supabase/ssr'
export default async function RootLayout({ children }) {
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookies().get(name)?.value
},
},
}
)
// Use this single client instance throughout the request
return <html>{children}</html>
}Avoid instantiating multiple serverClient instances in different layouts or components that run in parallel.
For applications experiencing legitimate concurrent refresh attempts, you can adjust the reuse interval in Supabase Dashboard:
1. Go to Authentication > Settings in your Supabase project dashboard
2. Navigate to Advanced Settings
3. Locate REFRESH_TOKEN_REUSE_INTERVAL (default: 10 seconds)
4. Increase slightly if you have complex SSR scenarios (not recommended to exceed 30 seconds)
Note: Only increase this value if you understand the security implications. The default 10-second window is sufficient for most legitimate use cases.
If users are stuck with invalid tokens, clear their session data:
// Client-side cleanup
await supabase.auth.signOut()
// Clear any additional cached data
localStorage.removeItem('supabase.auth.token')
sessionStorage.clear()
// Redirect to login
router.push('/login')This ensures users start with a clean slate and obtain fresh, valid tokens.
The refresh token reuse detection mechanism is part of Supabase's security architecture and follows OAuth 2.0 best practices for token rotation. When a refresh token is used, Supabase issues a new access token AND a new refresh token, invalidating the old refresh token immediately (with the 10-second grace period).
In production applications, consider implementing these additional patterns:
Token Management Mutex: Use a mutex or lock mechanism to ensure only one refresh operation happens at a time, particularly in applications with heavy parallel data fetching:
let refreshPromise: Promise<Session> | null = null
async function getValidSession() {
if (refreshPromise) return refreshPromise
refreshPromise = supabase.auth.getSession().then(async ({ data }) => {
if (needsRefresh(data.session)) {
const { data: refreshed } = await supabase.auth.refreshSession()
return refreshed.session
}
return data.session
}).finally(() => {
refreshPromise = null
})
return refreshPromise
}Disable Reuse Detection: While not recommended for production, you can disable refresh token reuse detection entirely in Auth Settings > Advanced Settings. This removes the security benefit but may be useful for debugging race condition issues.
Mobile Offline Considerations: Mobile applications should implement offline token storage carefully. When the app comes back online with an expired session, catch the refresh error and trigger re-authentication flow rather than attempting to reuse potentially stale tokens.
For more details on session management, refer to the official Supabase documentation on User Sessions and Server-Side Auth.
email_conflict_identity_not_deletable: Cannot delete identity because of email conflict
How to fix "Cannot delete identity because of email conflict" in Supabase
mfa_challenge_expired: MFA challenge has expired
How to fix "mfa_challenge_expired: MFA challenge has expired" in Supabase
conflict: Database conflict, usually related to concurrent requests
How to fix "database conflict usually related to concurrent requests" in Supabase
phone_exists: Phone number already exists
How to fix "phone_exists" in Supabase
StorageApiError: resource_already_exists
StorageApiError: Resource already exists