Demo, all content is generated
Question

Users get logged out randomly, logs say Invalid Refresh Token: Already Used

Solved · 1458 views · asked by sophie_l · edited

Three of my testers complain they get kicked out a few times a day. In the Supabase auth logs I see a lot of:

AuthApiError: Invalid Refresh Token: Already Used

I don't understand how a token can be used twice when I only log in once. Is this a Supabase bug? I'm on Next.js 15 with @supabase/ssr.

What I’ve tried

Increased JWT expiry to 24 hours (it made it less often but not gone). Asked Claude Code, it suggested calling refreshSession() in a useEffect every 10 minutes which I think made it worse.

Comment
Do you call refreshSession or getSession anywhere yourself? And what does your middleware matcher look like? jb_supa · edited
Yes, Claude added a refreshSession interval. Matcher I'll check. sophie_l · edited

3 answers

Marked as helpful by the asker
jb_supa · edited

Not a bug. Refresh tokens are single use: using one returns a new pair and the old one is dead. If two things refresh at the same moment with the same old token, the second one gets 'Already Used' and Supabase treats it as possible theft and ends the session.

In Next.js the usual causes:

  1. No middleware, so every Server Component tries to refresh on its own, and can't save the result to cookies. Next request uses the old token again.
  2. Your refreshSession() interval racing the automatic refresh of the browser client. Remove it; the client already refreshes by itself.

Fix: one middleware that calls supabase.auth.getUser() (the official updateSession example), no manual refreshes anywhere, and set your JWT expiry back to the default. Long-lived access tokens are a security trade-off you don't need.

Comment
I did have middleware but its matcher excluded /dashboard/*, which is where everyone lives. Fixed the matcher, removed the interval. Two days, zero logouts. sophie_l · edited
There's also a 'refresh token reuse interval' setting (a few seconds) that tolerates small races. Leave it on, but the real fix is what jb said. hannah_reyes · edited
dev_ana · edited

Also check you create the browser client once (a module-level singleton), not inside every component render. Several clients in one tab = several auto-refresh timers.

Comment
abby_ops · edited

Since the matcher was the culprit, here's the one I use. It runs on everything except static files and images:

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
}

Excluding app routes to 'save function invocations' is exactly how you get stale sessions.

Comment