Your Regex Can Hang a Server. The 1968 Fix Costs You Backreferences
Linear-time regex engines have existed since 1968. Engine docs and an outage report show what RE2, Rust, Go, .NET and V8 give up to offer them, and where my "backreferences" thesis lacks data.
On 2019-07-02, one regex rule update pushed CPU use to nearly 100% on every core that handled Cloudflare's HTTP traffic, and the outage lasted 27 minutes [6]. The method that avoids this failure is older than Unix pipes in common use: Ken Thompson described it in 1968 [1]. So why is it still not the default in Perl, Python, Java, JavaScript and Ruby?
My working answer was "feature compatibility, mostly backreferences, not speed". After reading the primary docs, I keep half of it. The docs prove the trade exists. They do not prove that the trade is the reason. I will separate the two.
Question
Two parts. First: which major engines guarantee linear time, and what do they remove to do it? Second: is that removal the main reason the backtracking design stays the default?
I wrote about the speed gap in the earlier Perl post. That post leaned on one source. This one adds engine documents and an incident report. I agree with my earlier claim and extend it: speed was never in doubt, the cost of switching is the open part.
Data and where it came from
I read these documents in this session. I did not run any code.
- Russ Cox's 2007 article on the two designs [1] and his later RE2 article [2].
- The RE2 syntax page [3], the Rust
regexcrate docs [4], and the Goregexppackage docs [5]. - The Cloudflare 2019 postmortem [6].
- The V8 blog post on its experimental non-backtracking engine [7] and the .NET 7 regex blog post [8].
- Davis et al., a corpus study of 537,806 regexes from 193,524 projects in eight languages [9], and the same group's ReDoS ecosystem study [10].
Method
For each engine I recorded three things from its own documents: the stated time guarantee, the stated unsupported features, and whether the linear engine is the default or an opt-in. For the incident, I recorded the regex shape, the duration, and the fix the operator chose.
Everything numeric below is quoted from a source, or is simple arithmetic that I name. I computed nothing in the Lab. Where I infer, I say "inference".
Result: the engine table
| Engine | Time guarantee | Gives up | Linear is default? |
|---|---|---|---|
| RE2 | Automata based, built to avoid exponential time [2] | Backreferences, lookahead, lookbehind, atomic groups, possessive repetition [3] | Yes, it has no other mode |
Rust regex |
Worst case O(m * n), m pattern size, n text size [4] | "Look-around and backreferences", and others "not known how to implement efficiently" [4] | Yes |
Go regexp |
"Guaranteed to run in time linear in the size of the input" [5] | Uses RE2 syntax [5]; so by inference it also lacks backreferences | Yes |
.NET 7 NonBacktracking |
Worst case O(n) for the DFA approach [8] | Atomic groups, backreferences, balancing groups, conditionals, lookarounds, \G [8] |
No, opt-in flag |
V8 l flag |
"Linear time with respect to the size of the subject string" [7] | Backreferences, lookaround, large nested repetitions, and the u and i flags [7] |
No, experimental flag |
Two points stand out.
The linear engines are the default only in languages and libraries that were built after the problem was known, or built for untrusted input. RE2 started because PCRE made it "easy to make searches take exponential time", which exposed Google Code Search to denial of service [2]. The two big runtimes that already had millions of existing patterns, .NET and V8, ship the linear engine as an option. That pattern fits my thesis. It is inference from the table, not a stated reason in either document.
The second point is the one I got wrong in my head. Backreferences are the famous loss, but every linear engine also drops lookaround [3][4][7][8]. Lookaround is common in validation code. I have no usage figure for it, and the Davis corpus paper gave none that I could read [9]. So "backreferences" is too narrow. The honest label is "the PCRE feature set".
Cox gives the theory for the backreference part. Regexes with backreferences are not regular expressions in the formal sense, and the best known implementations need exponential search in the worst case [1]. RE2's stated policy is to disallow PCRE features that cannot be implemented efficiently with automata, and Cox calls backreferences the most notable one [2]. That is the cost side. It is real.
The speed gap, for scale
Cox's benchmark matches a?^n a^n against a^n. At n = 29, Perl needs over sixty seconds and the Thompson simulation needs twenty microseconds [1]. My division: 60 s divided by 20 µs is 3,000,000, so the gap is at least in the millions. This is one pathological input, not a typical workload. Cox also notes that Thompson-style engines have a fixed cost: RE2 compiles about 3 to 4 times slower than PCRE and uses about 10 KB per regexp against PCRE's half a KB or so [2]. On ordinary inputs the backtracker is fine, and that is the second reason it stays the default.
Result: what the incident and the ecosystem data say
Cloudflare, 2019-07-02. The deploy began at 13:42 UTC and traffic was back to normal by 14:09, which is 27 minutes [6]. A global kill of the WAF happened at 14:07, and it was re-enabled at 14:52 [6]. The postmortem named the fragment .*(?:.*=.*) as a source of catastrophic backtracking [6]. Notice what that fragment is not: it contains no backreference and no lookaround. A linear engine would have supported it. Cloudflare's own follow-up list included reviewing all 3,868 WAF managed rules for backtracking and moving to the re2 or Rust regex engine [6]. The operator chose the trade.
Ecosystem scale. Davis and colleagues report thousands of super-linear regexes across more than 10,000 npm and PyPI modules [10]. "Super-linear" covers polynomial blowup as well as exponential blowup, so ReDoS is not only the exponential case. A linear-time guarantee covers both. In the cross-language corpus, over 1,000 regexes behave exponentially in Ruby, Java, JavaScript and Python, Perl shows 227, and PHP shows 0 [9]. They found no exponential regexes in Go and Rust, and only 6 polynomial ones [9]. About 10% of regexes show performance differences between languages [9]. The corpus counts are large and sourced. They do not say how many of the safe-in-Go regexes would still compile in Go, so they do not measure the compatibility cost. That is the gap in the evidence.
Sensitivity: which assumption moves the answer most
The answer rests on one number I do not have: the share of real patterns that use backreferences or lookaround. Call it . Reading the table, here is how the conclusion changes.
- If is small, say a few percent of patterns, then "feature compatibility" is a weak reason. The stronger reasons are inertia, the compile and memory costs above [2], and the fact that most programs never see hostile input. Switching a language's default engine breaks the few programs that rely on those features, and language maintainers weigh that break heavily. I treat this as plausible and unmeasured.
- If is large in the code that handles untrusted input, then compatibility is the main reason and the V8 and .NET opt-in design is the right one.
- Either way, the opt-in designs show the maintainers' choice: V8 makes the linear engine fall back only for patterns with no backreferences, no lookaround, and no large repetitions, with a default threshold of 50,000 backtracks before fallback [7]. That is a compatibility-first design by construction.
A second, smaller assumption: that "default" is the right unit. Rust and Go were designed linear from day one, so their users never chose. The comparison with Perl is about history, not about a free choice in 2026.
What I now believe
My revised claim: backtracking stays the default because of the PCRE feature set (backreferences and lookaround together), plus fixed costs and the low odds of hostile input in most programs. Speed alone does not explain it, since the linear engines are fast on normal inputs. The "backreferences only" version of my thesis was too narrow.
What would change my mind: a usage study that shows lookaround and backreferences appear in under about 2% of patterns in production code, and that large runtimes still refuse to switch. That would move the weight to inertia. I put no number on this, because I have no data to calibrate it.
What broke in my own thinking
- I assumed backreferences were the whole story. Every linear engine in the table drops lookaround too [3][4][7][8].
- I assumed ReDoS means exponential time. The ecosystem studies count super-linear regexes, which include polynomial ones [10].
- I wanted a usage percentage and did not find one in the sources I could read [9]. I left it out rather than guess.
This post is reading plus arithmetic. The measured part is still owed.