How to fix refresh_token_already_used: Refresh token has already been used in Supabase
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.
Quick fix
Fixes refresh_token_already_used: Refresh token has already been used
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
}
})- Implement proper onAuthStateChange listener
- Add retry logic with re-authentication fallback
- Prevent race conditions in SSR applications
- Configure reuse interval if needed
import { createClient } from '@supabase/supabase-js'const supabase = createClient(supabaseUrl, supabaseKey)// Register listener at app initializationsupabase.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}})refresh_token_already_used: Refresh token has already been used
What this error means
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.
Typical symptoms
- Users get logged out unexpectedly after token refresh attempts
- Authentication fails with "refresh_token_already_used" error in console
- Multiple simultaneous requests triggering race conditions during token refresh
- Session termination immediately after attempting to refresh authentication
- Error occurs more frequently in SSR/SSG applications with parallel data fetching
- Mobile apps experiencing sudden logouts when coming back online after network interruptions
Common causes
- Multiple refresh requests initiated simultaneously causing race conditions (common in Next.js apps with serverClient used in multiple layouts)
- Network issues where refresh token response wasn't received by client, causing retry with already-used token
- Stale refresh tokens stored in browser or client being sent to server
- Missing or incorrectly configured onAuthStateChange listener not updating token state
- Parallel API calls in SSR triggering multiple token refresh attempts
- Client-side code attempting to refresh tokens before server-side refresh completes
How to fix "refresh_token_already_used: Refresh token has already been used"
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.
Advanced notes
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.
Frequently asked questions
- What does "refresh_token_already_used: Refresh token has already been used" mean?
- 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.
- How do I fix "refresh_token_already_used: Refresh token has already been used"?
- 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.
- What causes "refresh_token_already_used: Refresh token has already been used"?
- Multiple refresh requests initiated simultaneously causing race conditions (common in Next.js apps with serverClient used in multiple layouts) Network issues where refresh token response wasn't received by client, causing retry with already-used token Stale refresh tokens stored in browser or client being sent to server
- Is the "refresh_token_already_used: Refresh token has already been used" error serious?
- It is a medium-severity error โ more of a blocker than a risk. The steps above resolve it.
Official resources & further reading
- ๐Supabase Official Documentation
- ๐supabase.com - sessions(Official Docs)
- ๐supabase.com - advanced guide(Official Docs)
- ๐github.com - 18981(GitHub)
- ๐github.com - 755(GitHub)
- ๐github.com - 466(GitHub)