Package
firebase-frameworks 0.11.8 (packages/firebase-frameworks), running as the Cloud Run SSR backend created by firebase deploy for a frameworksBackend Hosting site. Framework is Next.js 14.2 (App Router), Node 24.
Summary
handleAuth in src/firebase-aware.ts keys its firebaseAppsLRU on uid, and that cache is configured with dispose: (value) => deleteApp(value). Concurrent requests carrying the same __session cookie therefore destroy each other's Firebase App while it is still being signed in, and every loser of the race throws app/app-deleted out of handleFactory, which has no catch. Express answers HTTP 500.
The framework never runs for those requests: handleAuth throws before frameworkHandle(req, res) is reached, so the response is Express's 176 byte default error body, not anything the app rendered.
The race
const firebaseAppsLRU = new LRU({
max: LRU_MAX_INSTANCES,
ttl: LRU_TTL, // 5 minutes
allowStale: true,
updateAgeOnGet: true,
dispose: (value) => { deleteApp(value); }, // (1)
});
const handleAuth = async (req, res) => {
// ...
let app = firebaseAppsLRU.get(uid);
if (!app) {
const random = Math.random().toString(36).split(".")[1];
const appName = `authenticated-context:${uid}:${random}`;
app = initializeApp(undefined as any, appName);
firebaseAppsLRU.set(uid, app); // (2) disposes whatever was under `uid`
}
const auth = getAuth(app);
if (auth.currentUser?.uid !== uid) {
const customToken = await adminAuth.createCustomToken(uid);
await signInWithCustomToken(auth, customToken); // (3) throws if (1) fired for this app
}
// ...
};
With N concurrent same-uid requests and a cold entry for that uid:
- All N call
firebaseAppsLRU.get(uid) and miss, because none of them has reached its set yet.
- All N build a distinct app name (the
random suffix) and call initializeApp.
- Each
set(uid, app) at (2) evicts the value the previous request just stored, and eviction runs dispose, i.e. deleteApp, on an app that request is still using.
- The N-1 losers are parked on
await adminAuth.createCustomToken(uid) at that moment. When they resume into signInWithCustomToken, their app is gone.
Only the last setter survives. N-1 requests return 500.
Observed in production
Six distinct uids over 14 days on one Next.js site. A representative burst, five simultaneous App Router link prefetches issued from one page right after sign in:
| request |
status |
latency |
/journal?_rsc=... |
500 |
0.162s |
/journal/<id>?_rsc=... |
500 |
0.205s |
/resources?_rsc=... |
500 |
0.223s |
/journal/new?_rsc=... |
200 |
0.969s |
/dictionary?_rsc=... |
500 |
0.237s |
and exactly four errors, one per failed request, with four different random suffixes for the same uid:
FirebaseError: Firebase: Firebase App named
'authenticated-context:<uid>:yhw01qpkh5m' already deleted (app/app-deleted).
at FirebaseAppImpl.checkDestroyed (@firebase/app/dist/esm/index.esm.js:439:33)
at get options (@firebase/app/dist/esm/index.esm.js:417:14)
at AuthImpl._getAdditionalHeaders (@firebase/auth/.../totp-5d40279f.js:3150:22)
at _performFetchWithErrorHandling (@firebase/auth/.../totp-5d40279f.js:928:13)
at _performApiRequest (@firebase/auth/.../totp-5d40279f.js:882:12)
at _performSignInRequest (@firebase/auth/.../totp-5d40279f.js:976:34)
at signInWithCustomToken$1 (@firebase/auth/.../totp-5d40279f.js:5772:12)
at signInWithCustomToken (@firebase/auth/.../totp-5d40279f.js:5815:28)
at handleAuth (file:///workspace/node_modules/firebase-frameworks/dist/firebase-aware.js:80:15)
4 apps created, 4 disposed by later sets, 4 x 500, 1 survivor. The identical 4 fail / 1 pass split reproduced for a second uid ten minutes later.
The client's immediate retry succeeds, because by then the entry is warm and every request takes the get hit path.
When it fires
Any burst of same-uid requests that lands on a cold cache entry:
- the first navigation after sign in;
- the first request after
LRU_TTL (5 minutes) of idle;
- the first burst on a newly scaled Cloud Run instance.
Next.js App Router makes this routine rather than exotic: it prefetches every in-viewport <Link> concurrently, so a single page load issues 5+ simultaneous same-uid requests, and Cloud Run containerConcurrency is 80 so they land on one instance and race each other.
Reproduction
- Deploy any Next.js App Router site with
frameworksBackend and the Firebase JS SDK present, so isUsingFirebaseJsSdk() is true and handleFactory wraps the handler.
- Sign in and set a
__session session cookie.
- Load a page with five or more
<Link>s in the viewport, or just fire five concurrent requests with the same cookie.
- Four of the five return 500 with a 176 byte body, and the logs show four
app/app-deleted errors.
Possible fixes
Any one of these closes it:
- Do not evict an app that is still in flight. Guard the store:
if (!firebaseAppsLRU.has(uid)) firebaseAppsLRU.set(uid, app); else deleteApp(app); so the first writer wins and later arrivals discard their own app instead of destroying somebody else's.
- Cache the promise, not the app. Store an in-flight
Promise<FirebaseApp> under the uid so concurrent requests share one initialization rather than each performing their own.
- Catch in
handleFactory. Even with 1 or 2, await handleAuth(req, res) should not be able to turn an auth-context failure into a 500 for a page that does not need the auth context. Falling through to frameworkHandle unauthenticated is strictly better than a blank 500.
1 or 2 is the real fix; 3 is worth having regardless, since today any throw inside handleAuth takes down the request.
Package
firebase-frameworks0.11.8 (packages/firebase-frameworks), running as the Cloud Run SSR backend created byfirebase deployfor aframeworksBackendHosting site. Framework is Next.js 14.2 (App Router), Node 24.Summary
handleAuthinsrc/firebase-aware.tskeys itsfirebaseAppsLRUon uid, and that cache is configured withdispose: (value) => deleteApp(value). Concurrent requests carrying the same__sessioncookie therefore destroy each other's Firebase App while it is still being signed in, and every loser of the race throwsapp/app-deletedout ofhandleFactory, which has no catch. Express answers HTTP 500.The framework never runs for those requests:
handleAuththrows beforeframeworkHandle(req, res)is reached, so the response is Express's 176 byte default error body, not anything the app rendered.The race
With N concurrent same-uid requests and a cold entry for that uid:
firebaseAppsLRU.get(uid)and miss, because none of them has reached itssetyet.randomsuffix) and callinitializeApp.set(uid, app)at (2) evicts the value the previous request just stored, and eviction runsdispose, i.e.deleteApp, on an app that request is still using.await adminAuth.createCustomToken(uid)at that moment. When they resume intosignInWithCustomToken, their app is gone.Only the last setter survives. N-1 requests return 500.
Observed in production
Six distinct uids over 14 days on one Next.js site. A representative burst, five simultaneous App Router link prefetches issued from one page right after sign in:
/journal?_rsc=.../journal/<id>?_rsc=.../resources?_rsc=.../journal/new?_rsc=.../dictionary?_rsc=...and exactly four errors, one per failed request, with four different
randomsuffixes for the same uid:4 apps created, 4 disposed by later
sets, 4 x 500, 1 survivor. The identical 4 fail / 1 pass split reproduced for a second uid ten minutes later.The client's immediate retry succeeds, because by then the entry is warm and every request takes the
gethit path.When it fires
Any burst of same-uid requests that lands on a cold cache entry:
LRU_TTL(5 minutes) of idle;Next.js App Router makes this routine rather than exotic: it prefetches every in-viewport
<Link>concurrently, so a single page load issues 5+ simultaneous same-uid requests, and Cloud RuncontainerConcurrencyis 80 so they land on one instance and race each other.Reproduction
frameworksBackendand the Firebase JS SDK present, soisUsingFirebaseJsSdk()is true andhandleFactorywraps the handler.__sessionsession cookie.<Link>s in the viewport, or just fire five concurrent requests with the same cookie.app/app-deletederrors.Possible fixes
Any one of these closes it:
if (!firebaseAppsLRU.has(uid)) firebaseAppsLRU.set(uid, app); else deleteApp(app);so the first writer wins and later arrivals discard their own app instead of destroying somebody else's.Promise<FirebaseApp>under the uid so concurrent requests share one initialization rather than each performing their own.handleFactory. Even with 1 or 2,await handleAuth(req, res)should not be able to turn an auth-context failure into a 500 for a page that does not need the auth context. Falling through toframeworkHandleunauthenticated is strictly better than a blank 500.1 or 2 is the real fix; 3 is worth having regardless, since today any throw inside
handleAuthtakes down the request.