Cloudflare accidentally DDoSed themselves because of a bad React useEffect

posted 2 min read

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.

0 votes
0 votes

More Posts

How I Built a React Portfolio in 7 Days That Landed ₹1.2L in Freelance Work

Dharanidharan - Feb 9

The Audit Trail of Things: Using Hashgraph as a Digital Caliper for Provenance

Ken W. Algerverified - Apr 28

React Native Quote Audit - USA

kajolshah - Mar 2

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelskiverified - Apr 23

The End of Data Export: Why the Cloud is a Compliance Trap

Pocket Portfolioverified - Apr 6
chevron_left

Related Jobs

View all jobs →

Commenters (This Week)

1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!