Fix Supabase: Invalid Refresh Token Already Used
Trace which client retains an old Supabase token, preserve refreshed response cookies, and let the affected browser sign in again without a global logout.
TL;DR#
Invalid Refresh Token: Already Used means Supabase rejected reuse of a refresh token; the message alone does not identify which component kept the stale value. Trace the browser and server refresh paths, persist every replacement cookie, and remove manual refresh loops. Supabase explicitly tolerates some reuse, so overlapping requests or multiple tabs are not sufficient explanations. If the affected session is no longer recoverable, reset that session locally and ask the user to sign in again.
Invalid Refresh Token: Already UsedThe original Supabase Auth issue describes an intermittent page-load error followed by failed attempts to sign in again. Its author closed the issue after finding a possible cookie-clearing workaround, while explicitly saying the workaround was unconfirmed. That report establishes the symptom; it does not prove a universal cause or a reliable fix.
This page addresses a refresh token that exists but has become unacceptable to Auth. If the session simply never reaches the server, begin with Supabase session persistence across requests. Missing storage and rejected reuse need different investigations.
Why it happens#
A refresh replaces the session's token pair. If one part of an application retains an earlier refresh token while another part continues advancing the session, later requests can present an obsolete credential. A discarded response cookie, an independently maintained token variable, or a second refresh implementation can create that divergence.
However, rotation is not an absolute rule that every repeated request fails. The official session documentation describes two exceptions: reuse within the configured interval, whose documented default is 10 seconds, and use of the parent of the currently active refresh token. The latter accommodates a client that missed a refresh response, including outside the interval. Supabase advises against changing the default interval.
Consequently, a single lost response does not inevitably terminate a session, and a second tab does not by itself explain this error. A client stuck farther behind the active session is a more useful hypothesis to investigate. The documentation says reuse outside the exceptions terminates the affected session and revokes its refresh tokens. That is a security response to possible credential theft, rather than a reason to turn rotation off.
The current Auth token service source still contains this exact error and explicit permitted-reuse branches. Server implementation details can evolve; do not assume a hosted project runs a particular development-branch commit. The documented contract, the project's settings and the actual response together define the useful diagnostic boundary.
Fix#
1. Find every owner of the refresh process#
Search the application before changing the token lifetime. This PowerShell command finds manual refresh calls, explicit token injection, older helpers and both SSR client factories:
Get-ChildItem -Path src -Recurse -File |
Select-String -Pattern 'refreshSession\(|setSession\(|createBrowserClient|createServerClient|auth-helpers|refresh_token'Check the equivalent application directories if the project does not use src. For each match, establish whether it reads the live SDK storage or a copied session captured earlier. A refresh token stored in React state, a background-job payload or a module variable does not become current automatically when the SDK rotates its own stored session.
Remove an application timer that unconditionally refreshes alongside the browser SDK. Remove refresh calls inside token-refreshed event handlers. Where a deliberate refresh is necessary, use the configured client's current session rather than repeatedly passing a previously captured token. The refreshSession reference explains that the method refreshes regardless of whether the current access token has expired; calling it on every render adds refreshes rather than checking identity.
Use the cookie-based browser and request-specific server clients consistently. The SSR browser client source provides a browser singleton by default, while the server client source configures request-driven refresh rather than a background refresh timer. A browser singleton is appropriate; a server singleton holding one user's session across requests is not.
2. Preserve the replacement cookies through the whole response#
In SSR, refreshing is only half the operation. The updated token must reach downstream server rendering and the browser. Supabase's current client setup guide requires updating request cookies for Server Components and response cookies for the browser. Returning a fresh response after those writes silently discards the browser's update.
The following is a complete session-update utility for a Next.js request boundary. It assumes the environment variables from Supabase's setup guide are already configured. Keep your existing route authorisation policy alongside this utility; successful refresh alone does not grant access to a protected page.
// lib/supabase/update-session.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let response = NextResponse.next({ request })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet, headers) {
for (const { name, value } of cookiesToSet) {
request.cookies.set(name, value)
}
response = NextResponse.next({ request })
for (const { name, value, options } of cookiesToSet) {
response.cookies.set(name, value, options)
}
for (const [name, value] of Object.entries(headers ?? {})) {
response.headers.set(name, value)
}
},
},
}
)
const { error } = await supabase.auth.getClaims()
response.headers.set('Cache-Control', 'private, no-store')
return { response, error }
}The callback also preserves the cache headers provided by the current SSR package. Private authenticated responses must not be shared through a CDN. Ensure any later redirect or response wrapper copies the cookie objects and relevant cache headers, or return the response above directly.
Wire the function into the framework's actual request entry point. Supabase's Next.js instructions specify middleware.ts and an exported middleware function for Next.js 15 and earlier; Next.js 16 uses proxy.ts and proxy. Keep the existing matcher covering routes that require session refresh. A correctly written utility cannot update cookies if the framework never calls it.
Use the returned error to choose the application's public, protected or recovery flow. Do not treat an arbitrary cookie or the user object from getSession as verified identity. The getClaims versus getSession explanation covers that separate authorisation boundary.
3. Recover the affected session without a global logout#
If only the server reports stale credentials, first let the browser SDK inspect its current state. The advanced SSR guide recommends deferring to the browser in this situation because it may already possess an updated token. An old in-flight server request should not automatically erase a newer working browser session.
When the browser also has an unrecoverable refresh error, offer an explicit reset action using the same configured browser client. This small utility ends the affected session and leaves the sign-in UI to your application:
// lib/supabase/reset-browser-session.ts
import { createBrowserClient } from '@supabase/ssr'
export async function resetBrowserSession() {
const supabase = createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
)
const { error } = await supabase.auth.signOut({ scope: 'local' })
if (error) throw error
}Call it from a recovery button, handle a returned failure visibly, then show the normal sign-in form. Do not run it automatically for timeouts, network failures or every unauthenticated page view. The SSR troubleshooting documentation warns that signOut without a scope defaults to global and revokes other sessions. Local means the current session, which may be shared by tabs; it does not promise that another tab using that same session remains authenticated.
Avoid deleting every cookie on the domain or clearing all local storage. Those actions remove unrelated preferences and application sessions, and they conceal which storage adapter was wrong. Let the correctly configured SDK manage its own storage, including chunked cookies. A custom adapter must implement its removal operations correctly; changing the expiry of an unrelated cookie is not a session reset.
Verify the fix#
The response-loss mechanism was reproduced locally with Next.js 15.5.23 in a Node container. A response received a synthetic cookie; replacing that response discarded it. Returning the original response preserved it, and copying the cookie object to a redirect also preserved it. No Supabase server was contacted and no real token was used. This verifies the HTTP response mistake, not the hosted Auth issue or a hosted successful sign-in.
For the application itself, use a dedicated test account and its normal expiry settings. Sign in, allow the access token to approach expiry, navigate through a route that refreshes, and inspect the response in the browser's network panel. Confirm that replacement cookies are delivered and used on the next request. Inspect cookie presence, names and attributes without copying their values into a report.
Then test simultaneous navigation, a reload and a second tab. Successful overlapping requests are the expected control under Supabase's reuse allowances; their mere existence is not evidence of a bug. If failures persist, correlate request identifiers, route names, error codes and timing with the browser/server path that refreshed. Do not log cookies, request bodies containing credentials, or refresh token values.
Finally, test recovery after the affected browser's session is genuinely unusable. Check that it reaches sign-in, can establish a fresh session and survives another request. Verify that a separate device session remains usable when local recovery is chosen. If the failure instead becomes a retryable network error, continue with Supabase auth error handling in middleware; repeated local resets will not repair connectivity.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Why increasing the grace interval can hide the defect#
A longer interval may make a race less visible while leaving the application with two competing session stores. Disabling reuse protection can similarly suppress one symptom without ensuring that the newest token reaches the browser. Neither change repairs an already rejected session, and neither proves which component lost the update.
Keep the documented security behaviour while repairing session ownership and cookie propagation. If the error remains after those checks, provide Supabase support with a redacted request timeline, SDK versions, error codes and the relevant cookie adapter code. That evidence distinguishes a persistent stale-client path from an SDK or hosted-service defect without exposing the credentials the protection is designed to defend.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Supabase Auth Session Disappears on Refresh? Fix (2026)
Supabase auth sessions mysteriously disappearing after page refresh? Learn the exact cause and fix it in 5 minutes with this tested solution.
Fix: Supabase redirectTo Ignored on Magic Link / Email Auth
Magic links and password-reset emails that always land on the production Site URL instead of the redirectTo you passed. The allow-list mechanism behind it, and the wildcard pattern for local dev and preview branches.
Supabase getClaims() vs getSession(): The Silent Auth Bug
Your auth looks correct but users can bypass it. The bug: getSession() in server code reads from local storage without revalidating the token against the Auth server.


