The Regex That Broke Cloudflare: ReDoS Explained
One regex dropped 80% of Cloudflare's traffic in 2019. How catastrophic backtracking works, how to spot it in your code, and the deploy lesson behind it.

At 13:42 UTC on 2 July 2019, Cloudflare pushed a new firewall rule. Twenty-seven minutes later, roughly 80% of the traffic crossing its network was gone.
No attack. No hardware failure. One regular expression, shipped by someone doing completely ordinary work.
I keep coming back to this one because the failing code is the kind almost everyone has written. It's a pattern for catching cross-site scripting, and it looks fine. It passed tests. The problem only shows up when a specific input meets a specific pattern shape, and by then you're not debugging, you're watching every CPU in your fleet sit at 100%.
What actually happened on 2 July 2019
The timeline from Cloudflare's postmortem is worth reading slowly, because nothing in it looks reckless.
| Time (UTC) | Event |
|---|---|
| 13:31 | Engineer merges a pull request with a new XSS rule |
| 13:37 | CI builds the rules, tests pass |
| 13:42 | Automatic deploy begins, worldwide |
| 13:45 | First page fires |
| 14:00 | The WAF is identified as the source |
| 14:02 | Team proposes killing the WAF globally |
| 14:07 | Global kill executed |
| 14:09 | Traffic and CPU back to normal |
Three minutes from deploy to first alert. The rule was even meant to go out in simulate mode, where traffic passes through and nothing gets blocked. Simulate mode still has to run the regex to decide what it would have done, so "not blocking anything" saved nobody.
Here's the part that made it global instead of regional. Cloudflare shipped software through staged rollouts, moving through internal environments and canaries before reaching everyone. WAF rules did not go through that. They went out through Quicksilver, a distributed key-value store that pushes changes worldwide in seconds, and the standard procedure for a rule change specifically allowed that. The reasoning was sound on its face. When a new attack is going around, you want the rule protecting customers now, not in forty minutes.
Two release paths in one company. One had brakes. The one that ran attacker-shaped pattern matching against every request in flight did not.
The regex that did it
The rule was a long alternation, and this is the fragment that mattered:
.*(?:.*=.*)Cloudflare's own writeup simplifies it to .*.*=.*, which is the honest version of the problem. Three greedy .* groups with a literal = wedged between them.
Read it as a human and it says "some stuff, then more stuff, then an equals sign, then whatever." Read it as a backtracking engine and it says something much more expensive.
Why .*.*=.* is so expensive
.* is greedy. It grabs everything it can, then gives characters back one at a time when the rest of the pattern fails to match.
With one .* that's cheap. With two in a row before the =, the engine has to try every way of splitting the input between them. Match a string of length n and you get roughly n²/2 ways to divide it. Add a third .* after the = and it gets worse again.
The numbers Cloudflare published make it concrete:
| Pattern | Input | Steps |
|---|---|---|
.*.*=.* | x=x | 23 |
.*.*=.* | x= + 20 more x's | 555 |
.*.*=.*; | x= + 20 more x's | 5,353 |
Look at the third row. Adding a trailing ; that the input never satisfies took the same 20-character string from 555 steps to 5,353. That's the ugly case, because a failing match is the expensive one. The engine can't quit until it has proven no arrangement works, so it walks the entire search space first.
Now stop thinking about 20 characters. HTTP requests carry URLs, headers, and bodies that run to kilobytes, and the WAF ran this against real traffic.
This is ReDoS, whether or not anyone attacks you
Regular expression denial of service usually gets filed under security, because an attacker can send input crafted to hit the slow path. Cloudflare had no attacker. Ordinary customer traffic was enough. A pattern that can be weaponised can also just go off by itself.
Watch it happen
The clearest way to feel this is to time it. The demo below runs the failing variant against inputs of growing length and prints how long each one took.
Double the input and the safe pattern barely moves. The bad one takes about six times longer each time, because the work grows roughly with the cube of the input length rather than with the length itself. On my machine that's 0.6 ms at 100 characters and 130 ms at 800. Keep going and the tab stops responding, which is what happened to Cloudflare's CPUs at fleet scale.
How to spot this shape in your own code
You don't need a formal analysis to catch the common cases. Three shapes cause most real incidents.
Two unbounded quantifiers in a row. .*.*, \s*\s*, [a-z]*[a-z]*. If two adjacent parts of a pattern can both match the same characters, the engine has to try every boundary between them.
A quantifier inside a quantifier. (a+)+, (\d+)*, ([a-z]+\s?)+. This is the textbook exponential case, and it turns up constantly in hand-written validators for emails, URLs, and CSV-ish formats.
Alternation where the branches overlap, under a quantifier. (a|ab)+ and friends. Each repetition has more than one way to consume the same text, and the count multiplies.
The thing that turns any of these from slow into catastrophic is a match that fails near the end. A trailing ;, a $ anchor, a required suffix. That's the trap Cloudflare fell into, and it's why a pattern can look fine against your happy-path test fixtures and still take the site down.
Quick check
Which pattern is the dangerous one to run against untrusted input?
The fixes that actually work
Cloudflare's own remediation list is a decent template, and the most interesting item on it is the engine swap.
Use an engine that can't backtrack. RE2 and the Rust regex crate guarantee linear time by dropping the features that make backtracking necessary, mainly backreferences and lookaround. You give up a little expressiveness and you get a hard ceiling on run time. Go's standard regexp is RE2 based, so Go code already has this. Node users can reach for the re2 package and swap it in where patterns touch untrusted input. Cloudflare went this way, with a stated ETA of 31 July 2019.
const RE2 = require("re2");
// Same syntax, no catastrophic backtracking possible.
const re = new RE2(".*.*=.*;");
re.test("x=" + "x".repeat(10000)); // returns fastRewrite the pattern so only one path exists. Most greedy .* chains can be replaced with a negated character class, which gives the engine nothing to reconsider.
// Backtracks: two .* can split the input many ways
const slow = /.*.*=.*;/;
// Linear: [^=]* can only stop at the first =
const fast = /^[^=]*=[^;]*;/;Set a timeout where your language offers one. .NET takes a matchTimeout on the Regex constructor. Java and JavaScript give you nothing built in, so isolation is the answer there: a worker thread you can kill, or an engine that can't hang.
Bound the input before it reaches the pattern. Capping a header or field at a few hundred characters won't fix a bad pattern, but it does turn an outage into a slow request.
Cheap win for an existing codebase
Grep your source for .*.*, +)+, and *)*. It's crude and it has false positives, but on a codebase that has never looked, it usually finds something within a minute. Then check whether that pattern ever touches user-controlled input.
The lesson Cloudflare wrote down in 2019, and relearned in 2025
Here's the part that keeps this story useful instead of just memorable.
The 2019 postmortem got the diagnosis right. Cloudflare put back a CPU guard that had been removed earlier, hand-inspected all 3,868 rules in the managed set, added performance profiling to the test suite, and committed to staged rollouts for rules rather than instant global pushes.
Good list. Then read what happened on 18 November 2025.
A database permissions change made an internal query return duplicate rows. That doubled the size of the Bot Management feature file. The file propagated automatically across the network on its normal refresh cycle, hit a hard limit of 200 features against about 60 in real use, and the proxy handling it errored out. Core traffic failed for roughly six hours.
Different mechanism. Same shape. A non-code change went out to every machine on a fast path with no staged rollout, and a resource limit turned into a crash instead of a graceful degradation.
After that incident and another on 5 December, Cloudflare launched Code Orange: Fail Small, which commits to "controlled rollouts for any configuration change that is propagated to the network", a health-mediated deployment system that advances by stages and rolls back automatically, and defined failure behaviour for every module that handles traffic. The target was to have production systems covered by the end of Q1 2026.
Six years between writing the lesson down for rules and applying it to configuration and data generally. That gap is the actual story, and it isn't a Cloudflare problem. Almost every team I've worked with has a careful, gated pipeline for application code, and a completely different set of habits for the things that change behaviour without going through it. Feature flags. Remote config. ML model files. Rule sets. Denylists. Those all ship straight to production, often in seconds, often from a UI.
Ask yourself which changes at your company can reach every user without passing a canary. That list is usually longer than people expect, and it's where your next incident is sitting.
What to take away
The regex bug is the fun part of this story, and it's genuinely worth fixing. Learn the three dangerous shapes, prefer negated character classes over greedy chains, and put an RE2-backed engine in front of anything parsing untrusted input.
The bigger lesson is duller and more valuable. Cloudflare's engineers didn't lack skill, and they didn't skip review. The rule was merged, built, and tested. What failed was that one class of change had a path to production without brakes, and it stayed that way for the thing that wasn't code long after it was fixed for the thing that was.
Config is code. Data files are code. If a change can alter how your system behaves for every user, it deserves the same staged rollout, the same automatic rollback, and the same kill switch as a deploy. Cloudflare had that kill switch in 2019, which is the only reason the outage lasted 27 minutes instead of an afternoon.
For more on how small assumptions turn into real incidents, the Hugging Face agent breach is a good companion read, and prompt injection covers the same theme for untrusted input reaching a system that trusts it too much.

Written by
Rhythm Bhiwani
Engineer and relentless builder, happiest reverse-engineering hard problems until they click.
Enjoyed this?
Tap the heart to leave some love.
Be the first to react
Comments
Join the conversation.
Loading comments…


