A one-line log query turned into one of the better bug hunts I have had. Same IP, same second, eighteen requests: eleven calls to /quota/summary, five to /user/me, repeated until the user left the screen.
The pairing was the clue. Those two endpoints were only ever called together, from one place:
const [summary, me] = await Promise.all([fetchSummary(), fetchMe()]);
So something was calling that block in a loop, and the loop had to be synchronous enough to fire before the previous response even finished.
01The chain
Written out, the cycle is obvious. Inside the codebase it was invisible, because every link looked reasonable on its own:
onAuthChange(state => {
if (state === 'ready') this.loadData(); // 1
})
loadData() -> fetchMe() // 2
fetchMe() -> setProfile(me.profile) // 3
setProfile() -> emit() // 4 broadcasts state again
emit() -> listeners get state === 'ready' // 5 back to 1
Every step is defensible. The listener wants to load data once auth is ready. fetchMe wants to cache the profile. setProfile wants to notify everyone that the profile changed. And emit wants to tell listeners the current state.
The bug is at step 5. emit broadcasts the current state, not the change. So a profile update — which has nothing to do with auth — looks identical to a fresh login.
02Why nobody noticed
Three reasons, all worth remembering:
- The screen worked. Data was correct, just refetched forever. No visible error, no crash.
- The traffic hid in the noise. 18,547 calls accumulated over eleven days. In a dashboard of daily totals that is a rounding error.
- It only happened on one page. Staying on the home screen produced no loop at all.
What made it findable was looking at distribution instead of totals: which endpoints, which second, which IP. Eleven calls to the same endpoint in the same second from one device is not a user pattern.
03The fix
Two changes, four lines.
// 1. only act on the transition into 'ready'
let prev = '';
onAuthChange(state => {
const becameReady = state === 'ready' && prev !== 'ready';
prev = state;
if (becameReady) this.loadData();
});
// 2. do not broadcast when nothing changed
function setProfile(next) {
const unchanged = profile && JSON.stringify(profile) === JSON.stringify(next);
profile = next;
if (!unchanged) emit();
}
The first breaks the cycle. The second removes most of the noise that was causing it. Either alone would have stopped the loop; together they stop the class of bug.
04What I would do differently
- Name the event after the change.
onAuthStateChangeandonProfileChangeinstead of oneonChange. The bug is impossible to write when the names disagree. - Debounce anything that revalidates. A second-level guard on the fetch would have hidden the design flaw — but it also would have capped the damage at one request instead of eleven.
- Watch per-endpoint rates, not totals. A single counter would never have shown this. A per-path, per-minute breakdown shows it in one line.
The fix took four lines. Finding it took a log query that grouped by path and second. That ratio is normal, and it is the argument for keeping boring, queryable access logs.
Worth keeping
- Broadcast on change, not on emit — otherwise a profile update looks like a login.
- Guard handlers with a transition check, not a value check.
- Look at rate per endpoint when traffic looks wrong. Totals hide loops.