Repository navigation
Dev instances: clerk-js rewrites __clerk_db_jwt non-atomically, concurrent requests fail with dev-browser-missing #10411
Description
Preliminary Checks
- I have reviewed the documentation: https://clerk.com/docs
- I have searched for existing issues: https://github.com/clerk/javascript/issues
- I have not already reached out to Clerk support via email or Discord
- This issue is a bug report directly related to Clerk
Reproduction
No separate repository: the intermediate cookie state can be reproduced deterministically from the browser console in any Next.js app on a development instance (steps below). The race itself shows up in our Playwright suite on CI.
Publishable key
Development instance; available on request.
Description
On development instances, clerk-js rewrites the dev browser cookie non-atomically. A request to the app that leaves the browser during the rewrite carries __clerk_db_jwt but not __clerk_db_jwt_<suffix>, and clerkMiddleware treats it as signed out with reason dev-browser-missing, even though the session cookies are present and valid.
Where it comes from (clerk-js 6.40.1, dist/clerk.js, dev browser cookie handler):
o = () => { let e = iR(r); for (let t of [s, i]) t.remove(e), t.remove(), t.remove(iN) },
// ...
set: e => { /* ... */ o(), s.set(e, {...}), i.set(e, {...}) }s is the suffixed cookie and i the unsuffixed one. Every set first removes both (three removals each), then sets the suffixed one, then the unsuffixed one. set runs on every FAPI response carrying a Clerk-Db-Jwt header (onAfterResponse), so it happens several times right after each page load (client, token, organization lists) and on every token refresh.
On the server, AuthenticateContext.initHandshakeValues() (@clerk/backend 3.23.2 and 3.26.2) reads the dev browser token with getSuffixedOrUnSuffixedCookie, which only looks at __clerk_db_jwt_<suffix> when usesSuffixedCookies() is true. With the suffixed cookie missing, authenticateRequestWithTokenInCookie returns DevBrowserMissing. For a document GET the handshake recovers, but:
- a
fetchto a route handler gets a 401; - a server action gets redirected to the sign-in URL and silently has no effect;
- an RSC prefetch caches the signed-out payload, so a later client navigation lands on the sign-in page.
Evidence: we instrumented signedOut() in @clerk/backend to log the cookie names it saw when it returned signed out. Every dev-browser-missing case had __session, __session_<suffix>, __client_uat, __client_uat_<suffix> and __clerk_db_jwt, but no __clerk_db_jwt_<suffix>, which is exactly the state between s.remove(...) and i.remove(...) above. We also saw (less often) requests with neither variant, which is the state between the last removal and s.set(...). The window widens under CPU load: it hit our E2E suite several times per run on 2-vCPU CI runners, and we reproduced it locally by running Playwright with 6 to 8 workers.
Steps to reproduce (deterministic, simulating the intermediate state):
- Run a Next.js app with
clerkMiddlewareon a development instance and sign in. - In the browser console, remove only the suffixed dev browser cookie and call any route that goes through the middleware:
const suffixed = document.cookie.split('; ').find((c) => c.startsWith('__clerk_db_jwt_')).split('=')[0];
document.cookie = `${suffixed}=; Max-Age=0; path=/`;
const r = await fetch('/any-route-behind-clerkMiddleware');
console.log(r.status, r.headers.get('x-clerk-auth-status'), r.headers.get('x-clerk-auth-reason'));
// -> signed-out, dev-browser-missing (401 for an API route)- Reload: clerk-js writes the cookie again and everything works.
Expected behavior:
Rewriting the dev browser cookie should never leave a window where a signed-in browser sends an incomplete set. For example, set the new values first and only then remove stale variants with different attributes (or skip the removal when attributes don't change). On the backend, falling back to the unsuffixed __clerk_db_jwt when the suffixed one is missing would also close the most common window, since clerk-js always writes both with the same value.
Actual behavior:
Intermittent signed-out / dev-browser-missing for requests that carry a valid session, with silent failures in server actions and fetches.
Workaround we use: with a pk_test_ key, our middleware copies __clerk_db_jwt into __clerk_db_jwt_<suffix> (suffix taken from the __session_<suffix> cookie) when only the unsuffixed one is present, before calling clerkMiddleware.
Environment
@clerk/nextjs: 7.9.15
@clerk/backend: 3.23.2 (same lookup in 3.26.2)
@clerk/clerk-js: 6.40.1 (loaded from the instance CDN, latest 6.x)
@clerk/testing: 2.2.45
next: 16.4.1 (also seen with 16.4.0), proxy (middleware) on the Node.js runtime
Browser: Chromium via Playwright 1.64
OS: Ubuntu 24.04 (GitHub Actions, 2 vCPU) and macOS 27
Node.js: from .node-version (24.x)
Activity