Catastrophic Backtracking Killed Cloudflare Once. What Should Your Regex Tester Be Showing You?
Published on May 20, 2026 by The Kestrel Tools Team âą 9 min read
You ship a regex on a Tuesday. It passes every unit test. It works on every URL you can think of. Then on Friday afternoon, a single weird user-agent string hits the endpoint and one of your Node workers pegs a CPU at 100% for 14 seconds before the orchestrator finally kills it. Logs show nothing useful â just a request that never returned. You stare at the regex. It looks fine. It is not fine.
This is regex catastrophic backtracking in JavaScript, and itâs the same class of bug that took Cloudflareâs global edge offline for 27 minutes on July 2, 2019. One pattern, one input, one CPU. The fix is almost always a five-character change to the regex itself. The hard part is recognizing the bug before it ships, because match-vs-no-match â the only thing most regex testers actually show you â tells you nothing about whether your pattern will explode.
This post walks through what catastrophic backtracking is, how to read a regex and spot a vulnerable one, and what a regex tester should be showing you beyond a green checkmark. If you want to follow along interactively, the Kestrel Tools Regex Tester runs your patterns entirely client-side and surfaces backtracking step counts on every test â paste any of the examples below and watch the counter spike.
What is catastrophic backtracking in regex?
Catastrophic backtracking happens when a regex engine has too many ways to match (or fail to match) the same input, and it tries all of them. Each ambiguous quantifier multiplies the work. With nested quantifiers on overlapping character classes, the work grows exponentially with the input length â so a pattern that runs in microseconds on a 10-character input takes seconds on a 30-character input and effectively never returns on a 50-character one.
The canonical minimal example is four characters long:
/(a+)+$/.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!');
That regex matches one or more as, grouped, repeated one or more times, anchored to the end. On the string of 31 as followed by !, the engine will try to match, fail at the !, then back up and try every possible way to split those 31 as between the inner a+ and the outer +. There are 2Âłâ° such splits. On a modern laptop, thatâs roughly 10 seconds of pegged CPU before the engine gives up â for a four-character pattern. Add one more a to the input and it doubles. This is the entire mechanism behind ReDoS (Regular Expression Denial of Service): an attacker doesnât need to find a clever payload, just an input one or two characters longer than your tests covered.
The Cloudflare incident: a regex that took down half the internet
The load-bearing real-world example is Cloudflareâs July 2, 2019 outage. A new WAF rule was deployed globally to detect a class of XSS payloads. The rule contained the pattern:
(?:(?:\"|'|\]|\}|\\|\d|(?:nan|infinity|true|false|null|undefined|symbol|math)|\`|\-|\+)+[)]*;?((?:\s|-|~|!|{}|\|\||\+)*.*(?:.*=.*)))
Reading that pattern, two things jump out: nested quantifiers ((...)+ containing alternations of single characters that can also match the surrounding context), and the trailing (?:.*=.*) with two unbounded .* segments on the same line. Both of those are classic catastrophic-backtracking shapes. According to Cloudflareâs published RFO, the regex had been tested in their staging WAF against representative traffic â but no test input contained the specific shape that triggered the explosion. When real production traffic hit it, CPU usage on every Cloudflare edge machine went to 100% within seconds, and HTTP error rates globally spiked until the rule was rolled back.
The fix was to roll back the rule and then move the WAF off PCRE (which has unbounded backtracking) to a non-backtracking engine for new rules. But thatâs an organizational fix. The pattern-level lesson is the one most teams need: a regex that passes every test you wrote can still crash in production if you didnât test it against the specific shape of input that maximizes ambiguity.
How to spot a vulnerable regex by reading it
You donât need to memorize a list of bad patterns. There are three structural signals, and any one of them is a yellow flag worth a closer look.
1. Nested quantifiers on overlapping character classes. Anything of the shape (x+)+, (x*)*, (x+)*, or (x*)+ where the inner and outer can match the same characters. Examples: (a+)+, (\d+)*, (\w*)+$, (.+)+. The classic test is: can the same character be consumed by either the inner or the outer quantifier? If yes, you have exponential ambiguity.
2. Alternations where branches can match the same input. (a|a)+, (\d|\d+)*, (a|ab|abc)+ â the engine has to try every branch on every position, and if multiple branches accept the same prefix, itâll try all combinations. The Cloudflare regex is full of this: the inner alternation (\"|'|\]|\}|\\|\d|...) has overlap-friendly branches, all wrapped in a + quantifier.
3. Unbounded quantifiers on . (especially multiple in a row). .*X.* is fine. .*.*X or .*X.*X.* is not. Two or more .* segments anchored on the same line force the engine to consider every split of the input between them â same exponential explosion, just dressed differently. The Cloudflare regexâs trailing (?:.*=.*) is the same shape.
If you have any of these structures and the input can plausibly contain a long run of the matching character class, you have a potential ReDoS. The standard test is to construct an input thatâs a long run of the ambiguous character followed by one character that breaks the match â e.g. 'a'.repeat(40) + '!' for (a+)+$. If your regex takes more than a few milliseconds on that, ship it to staging and test there before production.
What a regex tester should be showing you (beyond match / no-match)
Most online regex testers show three things: matched / not matched, capture groups, and maybe a substitution preview. Thatâs enough to develop a regex, but itâs not enough to ship one safely. A regex tester for production work should also show:
- Backtracking step count. A counter for how many state transitions the engine took to either match or reject. A safe regex on a typical input is in the tens; a vulnerable one is in the millions. The number itself isnât the danger â exponential growth of the number as input length increases is.
- Time to match (and time to reject). Many ReDoS bugs are worse on the rejection path than the match path, because rejection has to exhaust every alternative before giving up. A tester that runs both a match and a forced-mismatch input shows the asymmetry.
- Pattern static analysis. Highlighting nested quantifiers, repeated
.*, and overlapping alternations â the three signals above â directly in the pattern, before the user even runs an input. - A âlong inputâ stress test. A button that runs the pattern against
'a'.repeat(50),'a'.repeat(100), and'a'.repeat(200)of the most-common character in the pattern, with a hard timeout. If the timeout fires, the pattern is suspect.
This is the lens we built the Kestrel Tools Regex Tester around. Paste a pattern, paste an input, and you get the standard match / capture groups view â plus a backtracking step count and a stress-test button that runs your pattern against escalating input lengths and surfaces any timeout. It runs entirely in your browser, so the patterns youâre testing (often security-sensitive WAF rules or data-validation logic) never leave your machine.
How to fix a vulnerable regex in JavaScript
The ECMAScript regex engine doesnât support atomic groups in the standard syntax (V8 added the v flag and Unicode property escapes, but atomic groups are still a Stage proposal as of 2026). That means the usual non-JS fixes â wrap the inner quantifier in (?>...) to forbid backtracking â donât work directly. The practical fixes for JavaScript are:
1. Restructure the pattern to be unambiguous. (a+)+$ should just be a+$. The outer + adds nothing â aaaa is already covered by the inner a+. This is the most common case and the easiest fix: read the pattern, spot the redundant outer quantifier, delete it.
2. Use possessive-style character classes. Replace overlapping alternations with a single, non-overlapping character class. (a|ab|abc)+ becomes [abc]+ if order doesnât matter for capture, which is ambiguity-free.
3. Anchor more strictly. .*X.* matching across a long string is much safer if you can replace one of the .* with a more specific class â e.g. [^\n]*X.* if X is supposed to be on a single line. Even better: use start-of-line anchors and tighten the pattern shape.
4. Use a non-backtracking engine. For high-stakes patterns (WAF rules, log parsing, anything ingesting untrusted input at scale), the right answer is often to move off PCRE-style engines entirely. Googleâs RE2 library is the gold standard here â guaranteed linear-time regex matching at the cost of some niche features. Thereâs no native RE2 in browser JavaScript, but re2js and the node-re2 package work in Node services.
The fix for the minimal (a+)+$ example is one character: drop the outer + to get a+$. The fix for the Cloudflare regex was a 27-minute outage and a multi-quarter rebuild of the WAF engine. The cost of catching it at the regex-tester stage is roughly five seconds.
A direct answer: how do you safely test a regex for catastrophic backtracking?
Run the regex against a long, ambiguous input with a hard timeout. Specifically: identify the most ambiguous character or class in the pattern, generate an input of 30, 50, and 100 of that character followed by one character that breaks the match, and run with a setTimeout-enforced cutoff (Nodeâs vm module or a worker thread is the cleanest). If any of the three escalating inputs hits the timeout, the pattern is vulnerable. This is the same protocol the Kestrel Tools Regex Tester runs automatically when you click âstress test.â
If you only do one thing differently after reading this post: any time you write or edit a regex that will run against untrusted input, run it once against 'a'.repeat(100) + '!' (or the ambiguous-character analog for your pattern). Five seconds of work, and youâve ruled out the entire class of bug that took Cloudflare offline.
The takeaway
Catastrophic backtracking is one of the few classes of bug where the symptom (an unresponsive request) is completely disconnected from the cause (a four-line regex shipped six months ago). Tests donât catch it because tests donât usually include 50-character runs of a single character. Code review doesnât catch it because the pattern looks fine. Production catches it, expensively.
The defense is structural awareness: nested quantifiers on overlapping classes, alternations with shared prefixes, multiple .* on the same line â these are the three shapes to spot. A regex tester that shows backtracking step counts and runs a stress test will catch the rest. Try the Kestrel Tools Regex Tester on the next regex you write; itâs free, client-side, and the entire stress-test protocol runs in one click.
If you want to feel the bug for yourself before you ship anything else: open a Node REPL, paste /(a+)+$/.test('a'.repeat(35) + '!'), and time how long it takes. Thatâs the exact same shape that took down half the CDN traffic on the internet for 27 minutes in 2019. The pattern is four characters long.