Short version: Cloudflare shipped a dashboard bug that rebuilt an object every render, stuck that object in a useEffect dependency, and the effect re-fired like crazy during a tenant-service deployment + retries. The Tenant Service got overwhelmed, auth failed everywhere, and the dashboard + API melted until they rate-limited, scaled up pods, reverted a bad change and shipped a hotfix with jitter.
Check the Cloudflare Blog to know more about it.
Now the long, nerdy, delicious part — React-first, infra-second, with practical code you can copy.
The Root of the Madness
At some point in Cloudflare’s dashboard code, they created an object every render, like so:
const params = { orgId: org.id, token: someToken() };
And then used that in a useEffect dependency array:
useEffect(() => {
fetchTenant(params);
}, [params]);
Problem? Every time the component re-renders, params is a brand-new object. React sees a new object reference every time and says, “Oh cool, let’s run this effect again.” And boom! repeated API calls, unending retries, and a tenant service overwhelmed by the spike.
Even worse: the dashboard had retry logic without jitter. So when a call failed, it retried aggressively, amplifying the storm of requests.
Why This Happens?
React checks dependencies with referential equality. It doesn’t care about the object’s shape, only whether it’s the same object in memory.
So this is bad:
const params = { orgId: org.id };
useEffect(() => {
fetchTenant(params);
}, [params]); // Triggers every render, even if org.id didn’t change
How To Do It Right, Some Practical Patterns
1) Depend on Primitives
Most of the time, just depend on the primitive values you need:
useEffect(() => {
fetch(`/tenant/${org.id}`);
}, [org.id]);
Simple, reliable, no surprises.
2) Memoize Objects
If you really need an object for a child component or hook:
const params = useMemo(() => ({ orgId: org.id }), [org.id]);
useEffect(() => {
fetchTenant(params);
}, [params]); // params won’t change unless org.id does
3) Use Callback for Functions
const loadTenant = useCallback(() => fetch(`/tenant/${org.id}`), [org.id]);
useEffect(() => {
loadTenant();
}, [loadTenant]);
You might be intrigued to just patch your broken code with useMemo and useCallback but do it with caution. Please know the cost of doing so.
4) Create Inside Effect
If the object is only needed for that effect:
useEffect(() => {
const params = { orgId: org.id };
fetchTenant(params);
}, [org.id]);
5) Refs for Stable Identity
const paramsRef = useRef();
paramsRef.current = { orgId: org.id };
useEffect(() => {
fetchTenant(paramsRef.current);
}, []); // Runs once
Bonus: Make Your Fetch Resilient
A resilient fetch includes exponential backoff + jitter:
useEffect(() => {
const controller = new AbortController();
let attempts = 0;
const tryFetch = async () => {
attempts++;
try {
const res = await fetch(`/tenant/${org.id}`, { signal: controller.signal });
if (!res.ok) throw new Error('Network error');
} catch (err) {
if (attempts >= 5) return;
const backoff = Math.min(1000 * 2 ** (attempts - 1), 30_000);
const jitter = Math.random() * backoff;
setTimeout(tryFetch, jitter);
}
};
tryFetch();
return () => controller.abort();
}, [org.id]);
Because synchronized retries are how you ruin a good Friday.
What Cloudflare Learned (That You Should Too)
- Single-line bugs in UI can take down production.
- Always be careful with useEffect dependencies — prefer primitives or memoization.
- Add backoff + jitter to retries.
- Don’t assume the server can handle infinite retry storms.
- Use monitoring to track weird retry patterns.
- Argo Rollouts would’ve helped auto-rollback bad deploys.
The TL;DR Moral of the Story
Frontend engineers, your code is a loaded gun in prod. Write your effects like you’re defusing a bomb: carefully.
Never trust that a new object won’t mess things up. Avoid retries without jitter. And for God’s sake, don’t ignore ESLint rules.