A refresh endpoint returns a new access token, but the next request still uses the old one. Reloading the page appears to fix it. Start by locating the first place the new state disappears: the refresh response, the callback return value, the browser cookie, or the component reading the session. Those observations lead to different fixes.
One NextAuth.js discussion reports this symptom with a Credentials provider, JWT sessions, and getServerSession. The report does not identify an exact package version or establish a universal root cause. Treat it as a useful debugging scenario, not proof that every stale session is the same library bug.
Record the version and the session reader
Copy the installed next-auth and Next.js versions from your lockfile into the incident notes. Record the session strategy, router, provider, and entry point that triggers refresh. “The server” is too broad: a Server Component, API route, and browser call to the session endpoint have different response paths.
The Auth.js v5 migration guide maps the v4 Server Component call getServerSession(authOptions) to v5 auth(). Client Components continue to use useSession() with a SessionProvider. For Node.js API routes, the comparison is getServerSession(req, res, authOptions) versus auth(req, res). The guide also says supplying res to auth() rotates the application session expiry. That does not establish that a downstream provider access token was refreshed.
Keep three lifetimes separate in your notes: application session, provider access token, and provider refresh token. A longer application session cannot make an expired provider token valid.
Trace one refresh without exposing credentials
Use one tab and one authenticated request first. In a development environment, arrange for the provider token to expire, then capture the following observations. Use request IDs, timestamps, and a diagnostic revision counter; keep raw tokens, cookie values, and authorization headers out of logs and shared screenshots.
| Boundary | Record | Question it answers |
|---|---|---|
| Refresh call | Request ID, start/end, status, new expiry | Did the provider or your backend accept the refresh? |
| Callback return | Incoming and outgoing diagnostic revision, expiry | Did the application return its updated state? |
| Browser response | Presence of Set-Cookie, cookie name and attributes |
Did a response reaching the browser attempt persistence? |
| Next request | Revision and expiry observed after session decoding | Did persistence survive a separate request? |
| Client session | Revision visible to the component | Is the UI reading the new session? |
Do not infer success from the first row. Your test passes only when the next request observes the expected state and the relevant API operation succeeds.
For JWT sessions, the v4 callback documentation describes the jwt callback as the place that returns token state. The session callback controls what reaches the client and only exposes a subset by default. Inspect both returns. A freshly calculated value in a local variable says nothing about a field omitted from the returned object. Conversely, a field absent from the client session may be deliberately server-only.
Keep refresh credentials out of client-readable session data. Auth.js’s JWT strategy can store them inside an encrypted, HttpOnly cookie; that differs from exposing them to component code through the session callback. A diagnostic expiry or revision is enough to compare the layers. The refresh-token guide describes both cookie and database storage strategies.
When the callback changes but the cookie does not
Check whether the refresh ran while rendering a Server Component. Current Next.js cookie documentation permits reading incoming cookies there but does not support setting cookies during Server Component rendering. Cookie mutation belongs in a supported Server Function or Route Handler, before streaming begins.
The v4 Next.js integration guide describes getServerSession updating cookies in supported contexts, while separately warning about the read-only Server Component context. Do not turn either statement into “every server session read persists changes.” Identify the exact caller and inspect its browser-facing response.
If the trace proves a missing write, move the refresh-and-persist operation to a supported authentication response path for your installed version. Preserve the authentication library’s cookie handling; manually recreating its encrypted session cookie introduces another implementation to maintain. Re-run the same two-request test after the change.
If Set-Cookie is present, inspect whether the browser accepted it and whether the next request carries the expected cookie. Compare domain, path, HTTPS, and cross-site context with the deployment. Check duplicate cookie names on different paths before changing callback logic. These are diagnostic branches, not evidence that cookie configuration caused the original report.
When persistence works but the component stays stale
Now compare a fresh session response with the rendered component. If the response is current, inspect whether the component uses session context directly or copied it into another state container that was never updated.
In the v4 client API, useSession().update() without an argument reloads the session from the server without a full page reload. It is useful after a confirmed server-side change. The same documentation warns that update() does not synchronize tabs in the way the provider’s refetch options do. Test a second tab separately.
Treat this as a client-state check, not a guaranteed provider-refresh operation. Sending a client-supplied access token or expiry into update(data) is also not proof of trustworthy state; validate any client input on the server. For v5, consult the reference for the installed package before transferring a v4 callback recipe unchanged. This workflow identifies where state is lost; it does not establish that an upgrade fixes the incident.
When the failure appears only with parallel requests
Repeat the test with two simultaneous requests, then two tabs. Compare which request began refresh, which response finished first, and which state the next request reads. A late response carrying older state is a different investigation from a response that never wrote state at all.
The Auth.js refresh-token guide explicitly warns about concurrent refresh attempts when a provider’s refresh token can only be used once. Its example also preserves the previous refresh token when a successful response omits a replacement. Check your provider’s contract and expiry units rather than assuming every backend matches the example.
If concurrency is established, design coordination around the shared credential and deployment. A process-local promise cannot coordinate independent processes. Decide where the authoritative refreshed value lives, how another request waits or retries, and how an older result is prevented from replacing a newer one. Test this behavior before increasing refresh frequency. The broader refresh-token race guide covers family state and retry recovery.
An Ory option when you want to move session ownership
Consider Ory when the objective is to move application login and session lifecycle into an identity service. In that design, replace the relevant Auth.js session boundary with Ory’s browser session flow and have the application validate that session. It is an architecture change, not an in-place repair for an Auth.js response that fails to write a cookie.
Ory’s session documentation describes a cookie issued by browser login and session inspection through /sessions/whoami or the SDK’s toSession. Browser applications, including server-rendered applications, are directed to use cookies. This gives you a concrete boundary to evaluate: the application consumes an Ory session instead of owning that same session through Auth.js callbacks.
Plan migration around identity mapping, login/logout screens, cookie routing, and what happens to existing signed-in users. Test revoked and expired sessions, an unavailable session service, and rollback before moving traffic. Prefer this option when that service boundary is useful beyond the current incident; retain Auth.js when correcting one response path solves the actual requirement.
Keep provider API access separate. Ory documents retrieval of initial social-provider tokens. That is not a promise that changing the application session system maintains your downstream provider token lifecycle. Assign ownership for provider refresh, secure storage, expiry, and error recovery explicitly.
Likewise, Ory’s session refresh flow can request reauthentication with refresh=true; that is distinct from exchanging a provider refresh token. Ory Hydra on Network supplies OAuth2 and OpenID Connect capabilities. Evaluate it when you need that protocol boundary, separately from Kratos-style application sessions.
Finish the incident with a small regression matrix: one request after expiry, a second request without reload, two concurrent requests, a second tab, and a rejected refresh. Record the expected session revision and recovery behavior for each. A page reload should become another test case, not the mechanism that keeps authentication working.
Sources are linked throughout this guide. Product capabilities can change; consult the linked documentation for your deployment.
Read our editorial approach ↗