This is a submission for DEV's Summer Bug Smash: Smash Stories powered by Sentry.
The Setup
Five months ago I shipped a feature I was genuinely proud of: anonymous auth for ReCode, so people could get persistent history and cross-device sync without ever creating an account. No signup friction, no passwords, just show up and your work is there next time. It worked. It kept working, quietly, for months.
Then one day I noticed something small and strange: my Firebase Auth console had 694 anonymous users. For an app with a handful of real daily sessions. That's not a rounding error — that's a ghost town of accounts nobody ever used, and somewhere in there, mine kept multiplying too.
The First Fix (That Wasn't)
The symptom was simple to describe and infuriating to pin down: after being idle for a while — tab closed, laptop asleep overnight — reopening the app sometimes handed me a brand new identity. Not an error. Not a crash. Just... a different UID than the one I'd been using an hour before, with all my saved history sitting orphaned under the old one.
My IndexedDB drafts never budged, not once in months — which was the first real clue. Whatever was breaking wasn't storage getting wiped. It was identity quietly splitting.
I found bug #1 fast: my initializeAuth() function registered a new onAuthStateChanged listener every time it was called, and never unsubscribed. If it got called twice before the first anonymous sign-in resolved — which happened routinely between app mount and my API client's lazy fallback — both listeners could see user: null and both fire their own signInAnonymously(). Two calls, two accounts, from one visit. Textbook race condition.
I fixed it: cache the in-flight promise so every caller shares one attempt, unsubscribe immediately so a listener can never double-fire. Deployed it. Watched the Firebase console. Felt very good about myself for about an hour.
Going Back to Basics
Then it happened again. Same day. Both dev and production got fresh, uninvited users — after the fix was confirmed live.
That's the moment a bug stops being annoying and starts being interesting. I made myself rule things out one at a time instead of guessing:
- Not React Strict Mode double-invoking effects — it happened in production too, where Strict Mode doesn't run.
- Not Vercel serving a different preview domain — checked the exact URL each time.
- Not a dev server port mismatch silently changing my browser origin.
- Not incognito, not a cookie-autodelete extension, not a Chrome privacy setting (I actually went and found the exact toggle I suspected — it only handles permission revocation, not storage).
- Not Firebase's own anonymous-account cleanup — that setting wasn't even switched on in my project.
Every single lead was a dead end. Which meant the real cause was somewhere I hadn't looked yet — and the only way to find it was to stop theorizing and start logging.
The Reveal
I ripped out the bare catch {} blocks that had been quietly swallowing the real error this whole time, and logged everything: every onAuthStateChanged transition, every signInAnonymously failure code, with timestamps.
Then I waited for it to happen again.
When it did, the console handed me the answer in one line:
Failed to load resource: the server responded with a status of 403 ()
securetoken.googleapis.com
[authState] null — no user
securetoken.googleapis.com. Token Service. Not sign-in — refresh.
I went and checked my Firebase browser API key's restrictions in Google Cloud Console. Months earlier, doing the responsible thing, I'd locked it down to only the APIs I thought my app actually used: Cloud Firestore, Identity Toolkit, Gemini. Reasonable. Except Firebase's SDK refreshes your session by calling Token Service on your behalf, silently, in the background — a dependency that never appears anywhere in my own code, so I never thought to allow it.
Every session worked fine right up until the SDK actually needed to refresh it. Then: 403. Silently rejected. onAuthStateChanged reports no user. My own code — reasonably, given what it could see — decides nobody's signed in and mints a fresh identity. Rinse, repeat, every single morning, for five months.
The Fix
Two lines of actual insight, in the end:
The code fix — cache the auth promise, unsubscribe properly:
+ let authReadyPromise = null;
export const initializeAuth = () => {
+ if (authReadyPromise) return authReadyPromise;
+ authReadyPromise = new Promise((resolve, reject) => {
- return new Promise((resolve, reject) => {
- onAuthStateChanged(auth, (user) => {
+ const unsubscribe = onAuthStateChanged(auth, (user) => {
+ unsubscribe();
if (user) resolve(user);
else {
signInAnonymously(auth)
.then(({ user }) => resolve(user))
- .catch(reject);
+ .catch((error) => { authReadyPromise = null; reject(error); });
}
});
});
+ return authReadyPromise;
};
The actual root cause — no code at all. Just adding Token Service API to my key's allowed list, right next to Firestore and Identity Toolkit.
The Cleanup
Fixing the leak doesn't un-orphan the 694 accounts it already left behind. I wrote a small Admin SDK script to page through every user in batches of 1000, filter down to the anonymous ones, and bulk-delete everything except the two UIDs I actually use — clearing five months of accidental sprawl in one run instead of clicking through 14 pages of a console table by hand.
What I Learned
The bug that actually mattered here was never in my JavaScript. It was in a security decision I was proud of — locking down an API key to "only what my app calls" — that quietly broke a dependency I never knew existed, because I never call it directly. Firebase does.
The bigger lesson: when something breaks intermittently and every obvious cause checks out clean, that's not a sign to guess harder. It's a sign to stop guessing and start logging the thing you assumed couldn't be the problem. The 403 had been sitting right there in the network tab the entire five months. I just hadn't been watching for it.
Top comments (0)