On September 12, 2025, Cloudflare experienced a major outage that left its dashboard and many of its APIs unavailable for over an hour. While the root cause, a small React hook misconfiguration, might sound surprisingly simple, the consequences were massive, affecting millions of users.
In this article, we’ll break down what went wrong, explain how the useEffect
hook works (and how it’s commonly misused), and share practical strategies for preventing similar cascading failures, especially when you’re building systems at scale.
The Outage in a Nutshell
At 16:32 UTC, Cloudflare deployed a new version of its dashboard containing a subtle but critical bug. The issue centered around a useEffect
hook that made repeated, unnecessary calls to the /organizations
endpoint (part of the Tenant Service API).
Just 85 minutes later, after a coinciding update to the Tenant Service itself, the API became overwhelmed. Because the Tenant Service is integral to Cloudflare’s API authorization layer, its failure caused widespread 5xx errors across the dashboard and APIs.
The outage lasted until 19:12 UTC, with a particularly painful twist: an attempted fix actually made things worse, triggering a second wave of instability.
The Culprit: A Misconfigured useEffect Dependency
The problematic code looked something like this:
const { useEffect, useState } = React;
const Dashboard = () => {
const [ data, setData ] = useState( null );
const params = { id: 123 };
useEffect( () => {
let aborted = false;
fetch( '/api/tenant' )
.then( ( res ) => res.json() )
.then( ( j ) => {
if ( !aborted ) {
setData( j );
}
} )
.catch( ( error ) => {} );
return () => { aborted = true; };
}, [ params ] );
return <div>{/* render dashboard */}</div>;
};
At first glance this seems reasonable, fetch tenant data when params
changes. But here’s the catch: params is a new object created on every render. Even though its contents ({ id: 123 }
) never change, JavaScript treats it as a different object each time because objects are compared by reference, not value.
How useEffect Compares Dependencies
React uses shallow reference equality to check if dependencies have changed:
- Primitives (strings, numbers, booleans): compared by value, safe to use directly.
- Objects, arrays, functions: compared by memory address, a new instance means it has “changed,” even if content is identical.
Because params
was re-created on every render, React saw a “new” dependency each time, causing the useEffect
to re-run endlessly. Each re-run triggered another API call, which, when multiplied across millions of active dashboard sessions, flooded the Tenant Service with traffic.
This wasn’t just a performance issue. It created a feedback loop: more API calls means more re-renders, meaning more API calls.
The Domino Effect
What happened with Cloudflare tried to recover caused an interesting case. After reverting the problematic code, all dashboard users were forced to re-authenticate. This will-intentioned mitigation triggered a Thundering Herd effect, where millions of clients simultaneously hit the login API, overwhelming the Tenant Service once again. This surge created a second wave of instability, turning the recovery attempt into a new failure mode.
Writing Safer useEffect Code
This incident is a textbook example of how a small frontend mistake can bring down critical infrastructure. Here’s how to avoid it:
1. Avoid Inline Objects in Dependency Arrays
Never include objects, arrays, or functions created inside the component body directly in useEffect
dependencies.
Bad:
const params = { id: 123 };
useEffect( () => { /* ... */ }, [ params ] );
Good:
const id = 123;
useEffect( () => { /* ... */ }, [ id ] );
Or, if you must use an object, stabilize it with useMemo
(but that’s a topic for another day).
2. Use ESLint’s react-hooks/exhaustive-deps Rule
This rule catches missing or unstable dependencies and forces you to think critically about what belongs in the array.
3. Prefer Primitive Dependencies When Possible
If your effect only depends on a few values, pass those values directly, not a wrapper object.
Mitigating Risk at Scale
When you operate at Cloudflare’s scale, even small frontend bugs can cascade into system-wide outages. The key is defense in depth: write stable React code (avoiding unstable dependencies in useEffect
), but also design your deployment and recovery processes to absorb shocks. That means rolling out changes in batches, adding observability to distinguish retries from new requests, and building client-side safeguards to spread the load instead of creating a traffic tsunami.
A Small Bug, a Big Lesson
Cloudflare’s September 12 outage wasn’t caused by a hacker, an outside DDoS attack, or a database crash. It was caused by a single line of React code that looked harmless, but actually wasn’t. Essentially, it was an internal DDoS attack.
This is a good reminder that in modern web applications, the frontend and backend are deeply intertwined. A UI bug can become an infrastructure emergency.
By writing more intentional useEffect
logic, adopting defensive deployment practices, and building observability into every layer, we can prevent small mistakes from turning into big outages.
After all, in software at scale, the smallest details carry the heaviest weight.
Thanks for reading, and happy (and safe) coding!