Introduction

Regular expressions are everywhere — they filter emails, validate passwords, search source code and power every text editor you have ever used. The basic idea is simple: describe a pattern, hand it to an engine, get back all the matching strings.

For the pure kind — the regexes that use only concatenation, alternation |, and the Kleene star * — matching is fast. A textbook algorithm converts any such pattern to a non-deterministic finite automaton (NFA), simulates all paths in parallel, and answers in time O(n×m)O(n \times m), where n is the text length and m is the pattern length. No exponential blowup, ever.

But modern regex engines add one extra feature that changes everything: backreferences. A backreference like \1 says "match whatever the first capture group just matched." That tiny addition — the ability to refer back to what was already captured — lifts the matching problem out of the polynomial world entirely. Matching a regex with backreferences against a string is NP-complete (proven by Aho, 1980).

Even without backreferences, PCRE-style engines that use recursive backtracking instead of NFA simulation can be catastrophically slow on carefully crafted inputs. A 30-character string can stall a server for seconds; a 60-character string for hours. That failure mode is called ReDoS — Regular Expression Denial of Service — and it has taken down production systems at Cloudflare, Stack Overflow and many others.

Watch the Backtracking Explode

The classic pathological pattern is (a+)+ matched against a string of aas followed by a character that cannot match (here we use XX). The backtracking engine must explore every way to partition the aas into groups — a number that grows exponentially with the string length. The NFA engine, by contrast, tracks all possible states in parallel and finishes in linear time.

<div class="controls">
  <label>{{input_length_label}} <b id="lenLabel">10</b> × 'a' + 'X'</label>
  <input type="range" id="lenSlider" min="1" max="28" value="10">
</div>
<div class="results">
  <div class="engine" id="btBox">
    <div class="eng-title">{{bt_engine_title}}</div>
    <div class="eng-sub">{{bt_engine_sub}}</div>
    <div class="steps" id="btSteps">—</div>
    <div class="label">{{steps_label}}</div>
    <div class="bar-wrap"><div class="bar bt-bar" id="btBar"></div></div>
  </div>
  <div class="engine" id="nfaBox">
    <div class="eng-title">{{nfa_engine_title}}</div>
    <div class="eng-sub">{{nfa_engine_sub}}</div>
    <div class="steps" id="nfaSteps">—</div>
    <div class="label">{{steps_label}}</div>
    <div class="bar-wrap"><div class="bar nfa-bar" id="nfaBar"></div></div>
  </div>
</div>
<div class="pattern-display">{{pattern_prefix}} <code>(a+)+X</code> {{on_input_prefix}} <code id="inputDisplay">"aaaaaaaaaX"</code></div>
<div class="note" id="note"></div>
<button id="runBtn" type="button">{{run_btn}}</button>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.controls { margin-bottom: 1rem; }
.controls label { font-size: .9rem; display: block; margin-bottom: .3rem; }
input[type=range] { width: 100%; accent-color: #1d3557; }
.results { display: flex; gap: 1rem; margin: 1rem 0; }
.engine { flex: 1; background: #f0f4f8; border: 1px solid #cdd9e3; border-radius: 10px;
          padding: .8rem; text-align: center; }
.eng-title { font-weight: 700; font-size: .95rem; color: #1d3557; }
.eng-sub { font-size: .75rem; color: #666; margin-bottom: .5rem; }
.steps { font-size: 2rem; font-weight: 800; color: #1d3557; line-height: 1; margin: .3rem 0; }
#btBox .steps { color: #c92f3c; }
#nfaBox .steps { color: #0a7d33; }
.label { font-size: .7rem; color: #888; text-transform: uppercase; letter-spacing: .05em; }
.bar-wrap { background: #dce4ec; border-radius: 6px; height: 10px; margin-top: .6rem; overflow: hidden; }
.bar { height: 100%; border-radius: 6px; width: 0%; transition: width .4s; }
.bt-bar { background: #e63946; }
.nfa-bar { background: #2d9e5e; }
.pattern-display { font-size: .85rem; color: #444; margin: .5rem 0; }
code { background: #e8eef3; padding: .1rem .35rem; border-radius: 4px; font-size: .85rem; }
.note { font-size: .88rem; min-height: 1.4em; font-weight: 600; margin: .4rem 0; }
.note.warn { color: #b5470a; }
.note.ok { color: #0a7d33; }
button { font: 600 14px system-ui; padding: .45rem 1.1rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; margin-top: .2rem; }
button:hover { background: #16294a; }
// Code not found

Slide the input length to see how quickly the backtracking step count dwarfs the NFA step count. At length 20 the difference is already staggering. This is not a pathological corner case — any pattern of the form (X+)+ or (X|X)* applied to a non-matching suffix triggers the same explosion.

The Real Complexity

The complexity of regex matching depends sharply on which features the pattern language includes:

  • Pure regexes (no backreferences): matching is in P. Convert the pattern to an NFA with ε-transitions, simulate it on the input string tracking all active states as a set, and you finish in O(n×m)O(n \times m) time. The key insight is that the NFA never needs to revisit a position, because all paths are explored in parallel.
  • Extended regexes with backreferences: matching is NP-complete. The proof (Aho, 1980) reduces 3-SAT to regex matching. A backreference constrains two captured substrings to be identical, which is powerful enough to encode arbitrary satisfiability constraints. So deciding "does string w match pattern p?" when p contains backreferences is exactly as hard as SAT.
  • PCRE with full backtracking: in the worst case the engine explores a tree of choices that is exponential in the input length — not even polynomial. Patterns with nested quantifiers over the same characters (like (a*)* or ([ab]+)*) create this behavior.

The gap is stark: a one-character extension to the syntax — \1 — moves the problem from a clean O(n×m)O(n \times m) algorithm to NP-hardness. Most programmers never encounter the theoretical boundary, but every production regex engine that uses backtracking is one crafted input away from it.

Status: NP-completeness of backreference matching is a proven theorem (Aho 1980, confirmed by Backurs & Indyk 2016). The polynomial NFA algorithm for pure regexes is a standard textbook result. ReDoS is an ongoing applied security concern.

Where It Matters

The gap between NFA-based and backtracking regex engines has concrete consequences across the software stack:

  • Security (ReDoS): a single malicious input can pin a server's CPU at 100% for minutes or longer. Cloudflare's 2019 global outage and Stack Overflow's 2016 downtime were both caused by pathological regexes in hot-path validators. Modern WAFs and input validators must either use NFA engines or run static analysis on every regex before deployment.
  • Static regex analyzers: tools like regexploit, vuln-regex-detector and safe-regex automatically detect patterns that can backtrack exponentially. They are now standard in security-conscious CI pipelines.
  • Compiler lexers: lexer generators (Flex, RE2, Java's Pattern with CANON_EQ disabled) compile patterns to DFAs, sidestepping backtracking entirely. The price is that they reject backreferences — a deliberate trade-off for speed and safety guarantees.
  • Bioinformatics: DNA and protein sequence search tools (BLAST, bowtie) use carefully bounded approximate matching rather than general regex engines, partly to avoid worst-case blowup on repetitive genomic sequences.
  • Program analysis: static analyzers that check whether a string variable can match a regex (to detect injection vulnerabilities) must decide regex membership, and the NP-hardness of extended patterns means they must either restrict the language or resort to heuristics.

Understanding the complexity boundary — pure regex in P, backreferences NP-complete — helps engineers pick the right tool. Prefer NFA-based engines like RE2 or Rust's regex crate when safety matters; reserve PCRE for contexts where the input is trusted and patterns are vetted.

Conclusion

Regular expressions look like a solved problem — and for pure regexes they are. The NFA simulation runs in O(n×m)O(n \times m), never backtracks, and handles any pure pattern you can write. That is a clean, fast, provably correct algorithm.

Add backreferences and the ground shifts. The matching problem becomes NP-complete, the polynomial algorithm breaks down, and real engines fall back to exponential backtracking. The result is not just theoretical: a single unlucky pattern in a server's input validator can render it unresponsive for minutes.

The lesson is simple but easy to forget: the syntax of your pattern determines its complexity class. Reaching for backreferences is reaching for NP-hardness. When you don't need them, an NFA-based engine like RE2 gives you all the expressiveness of pure regexes with an ironclad O(n×m)O(n \times m) guarantee — no crafted input can ever make it slow.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/regular-expression-matching-hardness/Content licensed under CC BY-NC 4.0.