useSession() Only Updates After a Manual Reload in the Next.js App Router
The user signs in. The credentials are correct, the request succeeds, the cookie is set. And the navbar still says "Sign in." They update their display name in a settings form, get a green success toast, and the name in the corner is still the old one. Nothing looks logged in until they hit F5, and then everything is suddenly correct.
You reach for the fix everyone posts: router.refresh(). Sometimes it works. Sometimes it does not, and you cannot see why the same call fixes one component and not the one right next to it.
The reason is that in the App Router there is not one session, there are two reads of it. One lives in a client-side cache; the other is read fresh on the server every render. A client-side sign-in updates the cookie but does not, by itself, refresh either read reliably. router.refresh() addresses exactly one of the two. Until you know which read each component uses, you are fixing half the problem and guessing about the rest.
Client components read the session from SessionProvider's in-memory cache; Server Components read the cookie fresh on each server render. A client-side sign-in or update() refreshes neither automatically.
- If the stale value is in a Server Component, you must call
router.refresh()after the change; nothing else re-runs the server render. - If the stale value is in a client component, call
update()fromuseSession()(or navigate) to refresh the cache. - The most reliable pattern is to complete authentication with a redirect or navigation, so the whole route tree re-renders against the new cookie.
The symptom: useSession only updates after a manual reload
Stated the way people search for it:
useSession only getting the session after manually reloading the page
There is no thrown error and no stack trace, which is part of why it eats hours. The code runs, the promise resolves, the sign-in "worked." The UI simply reflects a session state that is one step behind reality, and a manual reload hides the bug by forcing a fresh server render and a fresh cache fetch at the same time. Because a reload fixes both reads at once, it never tells you that there were two reads to begin with.
Why there are two sessions, and why they drift
Auth.js (NextAuth) exposes the session two completely different ways in the App Router, and they do not share state.
The client read. SessionProvider is a client component. On mount it fetches /api/auth/session once and holds the result in memory, handing it to every useSession() call from that cache rather than hitting the network each time. It refreshes that cache on its own schedule: on a window-focus event, on an interval if you configured one, and via a broadcast channel that syncs other tabs. Crucially, it does not refresh the instant a cookie changes underneath it. So right after signIn(), the cookie is new but the cache is old, and useSession() keeps returning the old value until something triggers a refetch.
The server read. A Server Component that calls auth() (v5) or getServerSession() (v4) reads the session cookie directly during the server render. It is always accurate for the render it runs in, but it only runs when the server renders that route. A client-side sign-in does not re-run any server render, so a Server Component keeps showing whatever it rendered with last, which was the logged-out state.
Now router.refresh() makes sense. It re-runs the Server Components for the current route and refetches their data, so it fixes the server read. It does not touch the SessionProvider cache and it preserves client component useState, so it does nothing for a stale client read. That is the entire reason it works for some components and not others: it only ever addresses one of the two paths.
The fix, decided by where you read the session
Step 1: Find out which read is stale
Before writing any fix, answer one question for the component showing the wrong value: is it a Server Component calling auth(), or a client component calling useSession()? The correct action is different for each, and applying the wrong one is why the internet's advice is a coin flip.
Step 2: Refresh the client cache for client components
After a programmatic sign-in that stays on the page, refresh the provider cache so useSession() re-renders with the new value. The update() function returned by the hook forces a refetch:
'use client'
import { signIn, useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
export function LoginForm() {
const { update } = useSession()
const router = useRouter()
async function onSubmit(values: Credentials) {
const res = await signIn('credentials', { ...values, redirect: false })
if (res?.ok) {
await update() // refresh the client-side session cache
router.refresh() // re-run Server Components on this route
}
}
}
Call both, in that order, because most real pages read the session in both a client hook and a server component. update() fixes the hook; router.refresh() fixes the server render.
Step 3: For Server Component staleness, router.refresh() is mandatory
If a server-rendered navbar shows the session via auth(), there is no client cache to poke. The only thing that updates it short of a full reload is a new server render, and router.refresh() is how you request one without losing client state. If you skip it, that navbar stays wrong until the user navigates or reloads.
Step 4: Prefer finishing auth with a navigation
The most robust pattern avoids the whole split. If you let sign-in redirect (or you router.push() to a new route on success), the destination renders fresh on the server with the new cookie already set, and the provider re-initialises its cache on the way. You get both reads correct with no manual coordination. Reserve the update() plus router.refresh() dance for cases where you genuinely must stay on the same page, such as an inline profile edit.
| Where the session is read | What refreshes it | What does not |
|---|---|---|
Client component via useSession() | update(), navigation, window focus | router.refresh() alone |
Server Component via auth() | router.refresh(), navigation, reload | update() alone |
| Both (typical real page) | update() then router.refresh(), or a redirect | either one on its own |
Verify it works without a manual reload
The test is simply: never touch the reload button.
1. Put a client component (useSession) and a server component (auth())
that both show the user's name on the same page.
2. Sign in through your form. Do NOT reload.
3. Both must switch to the signed-in state within a moment.
4. Change the display name via an update() flow. Do NOT reload.
5. Both reads must show the new name.
6. Open a second tab, sign out there, return to the first tab: it should
pick up the change on focus (the broadcast channel).
If step 3 or 5 fails for only one of the two components, you now know exactly which read you missed and which trigger to add. Test this across the server/client boundary deliberately, because a page that only has one kind of read will pass while a mixed page fails.
What people get wrong
Treating router.refresh() as a blanket cure. It refreshes Server Components only. If your stale value is in a client useSession(), it can do nothing, and you will conclude the call is "flaky" when it is doing precisely what it says.
Calling only update() when a server component is involved. The mirror image: you fix the hook, the server-rendered navbar stays stale, and you blame the cache.
Cranking refetchInterval down to a couple of seconds. This masks the timing by hammering /api/auth/session on every client, which is wasted requests and still leaves a visible lag. Refresh on the event that changed the session, not on a fast timer.
Copying the session into your own state. Mirroring useSession().data into a useState creates a third copy that drifts from both of the two you already had. Read from the hook, do not shadow it.
When it is still broken
- The cookie itself may be stale. If your
jwtcallback does not return the updated token, no amount of refreshing helps because the source of truth never changed. That is a different bug, the same family as token rotation dropping the new token. Confirm the cookie changed before blaming the reads. - An
update()with new data needs a matchingjwtcallback. When you pass data intoupdate(), yourjwtcallback receives atriggerof"update"and must merge that data into the token, or the refetched session will not contain it. - Middleware can cache a redirect. If auth-gated routing lives in middleware, a stale matched route can send a freshly-signed-in user back to the login page. Check the middleware sees the new cookie.
- Disabled focus refetch. If you set
refetchOnWindowFocus={false}, you removed one of the client cache's refresh triggers, so you must drive updates explicitly withupdate().
The mental model that makes this stop being mysterious: sign-in changes a cookie, and a cookie change does not automatically re-render anything. You have to tell the client cache and the server render, separately, that the world moved. Once you see the two reads, the "sometimes router.refresh() works" confusion disappears, because you can name which read each component uses and reach for the trigger that actually refreshes it.
Frequently asked questions
- Why does useSession() only update after I manually reload the page?
- Because SessionProvider caches the session in memory and only refreshes it on specific triggers (window focus, an interval, a tab broadcast, or an explicit update() call), not the instant a cookie changes. After signIn() the cookie is new but the cache is old, so useSession() keeps returning the previous value. A manual reload forces a fresh fetch, which is why it appears to fix everything.
- Why does router.refresh() not fix my stale session?
- router.refresh() re-runs Server Components and refetches their data, so it only fixes a session read that happens on the server via auth() or getServerSession(). It does not touch SessionProvider's client-side cache, so a stale useSession() in a client component is unaffected. If the stale value is in a client hook, call update() instead, or in addition.
- What is the most reliable way to update the session after sign-in in the App Router?
- Complete authentication with a redirect or a navigation to a new route. The destination renders fresh on the server with the new cookie already set, and SessionProvider re-initialises its cache, so both the server read and the client read are correct with no manual coordination. Only fall back to update() plus router.refresh() when you must stay on the same page.
- Do I need both update() and router.refresh() after signing in?
- On a typical page, yes. Most real pages read the session in both a client component (useSession) and a server component (auth()). update() refreshes the client cache and router.refresh() re-runs the server render, so calling both, in that order, covers both reads. A page with only one kind of read needs only the matching trigger.