NFA vs DFA: How Regex Engines Actually Work
Most regex engines backtrack through an NFA; a few compile to a DFA instead. The difference explains why some patterns hang forever and others never do.
A regular expression engine matches patterns one of two fundamentally different ways: by backtracking through a nondeterministic finite automaton (NFA), trying paths until one works, or by running a deterministic finite automaton (DFA) that tracks every possible match state at once and never backtracks at all. Almost every mainstream language — JavaScript, Python, PCRE-based tools — uses the first approach. It’s why a regex that looks perfectly reasonable can occasionally freeze a process for minutes.
What “nondeterministic” actually means
A finite automaton is a state machine: a set of states and transitions between them, driven by input characters. In a DFA, every state has exactly one transition per input symbol — given a character, there’s only ever one place to go. An NFA relaxes that: a state can have multiple transitions for the same input, or transitions that require no input at all (epsilon transitions), so at any point the automaton may be in several states simultaneously.
Regex syntax maps naturally onto NFAs. Alternation (a|b) is literally “try this transition or that one.” A backtracking engine implements this by picking one branch, following it as far as it can, and rewinding to try the next branch if the first one fails. That rewinding is backtracking, and it’s a depth-first search over every possible way the pattern could match.
Why backtracking engines won
NFA-with-backtracking is the default not because it’s faster — it usually isn’t — but because it makes certain features cheap to implement: backreferences (\1, matching whatever an earlier group captured), lookahead and lookbehind assertions, and possessive or lazy quantifiers all fall out naturally from a search that can pause, remember state, and try alternatives. A pure DFA can’t express backreferences at all — they require comparing runtime values, not just following fixed transitions — which is a formal limitation, not an implementation gap. Since most regex libraries wanted those features, backtracking became the practical default across JavaScript, Python, Java, and PCRE, even though it comes with a real cost.
DFA-based engines and the guarantee they buy
A handful of engines — Google’s RE2, Rust’s regex crate, Go’s regexp package — compile patterns to a DFA (or an NFA simulated without backtracking, which amounts to the same guarantee) using construction algorithms that date back to Ken Thompson’s original work on grep. The payoff is a hard bound: matching runs in time linear to the input length, no matter what the pattern looks like. There’s no code path where the engine can spiral into exponential work, because it never backtracks — it advances through the string once, tracking the whole set of possible states in parallel.
The cost is that these engines drop backreferences and some lookaround forms, since those genuinely can’t be expressed as a DFA walk. For most everyday patterns — validation, tokenizing, extraction — that’s not a meaningful loss.
Catastrophic backtracking, and why it happens
A backtracking engine’s worst case shows up with patterns that have ambiguous, overlapping ways to match the same input — classically, nested or adjacent quantifiers like (a+)+b or (a|a)*b. Fed a long string of as with no trailing b, the engine tries every possible split of those as across the repeated group before concluding there’s no match. The number of ways to partition the string grows exponentially with its length, so a pattern that matches instantly on a 20-character input can take longer than the process’s lifetime on a 40-character one.
This is the mechanism behind ReDoS (regular expression denial of service): an attacker doesn’t need to break anything, just submit a string engineered to trigger worst-case backtracking against a vulnerable pattern, tying up a request thread indefinitely. Because JavaScript’s regex engine runs on the same thread as everything else, a single catastrophic match blocks the event loop — the whole process hangs, not just that request, which makes it a real denial-of-service vector rather than a slow query.
NFA-backtracking vs DFA at a glance
| Backtracking (NFA) | DFA-based | |
|---|---|---|
| Worst-case time | Exponential in input length | Linear in input length |
| Backreferences | Supported | Not supported |
| Lookahead/lookbehind | Supported | Limited or unsupported |
| Typical engines | JavaScript, Python re, PCRE, Java | RE2, Rust regex, Go regexp |
| Failure mode | Can hang on pathological input | No pathological input exists |
Keeping backtracking safe
Since most day-to-day code runs on backtracking engines, the practical defenses are pattern discipline and limits, not switching engines: avoid nested quantifiers over the same characters, prefer atomic groups or possessive quantifiers where the engine supports them, anchor patterns so partial matches fail fast, and put a timeout around any regex evaluation on untrusted input. If a pattern’s complexity genuinely calls for backreferences or lookaround, that’s fine — just don’t run it against attacker-controlled strings without a bound on how long matching is allowed to take.
The takeaway
Regex matching isn’t one algorithm — it’s two different strategies with opposite trade-offs. Backtracking NFAs support the expressive features developers actually use, at the cost of exponential worst cases on adversarial input. DFA-based engines trade those features for a hard linear-time guarantee. Knowing which one your language runs explains both why certain patterns are dangerous and why RE2-style engines exist at all: not because they’re smarter, but because they refuse to backtrack in the first place.
Keep reading
The Lycoris Team · · 4 min read Tail Call Optimization Explained
Tail call optimization reuses a function's stack frame for its final call instead of pushing a new one, turning some recursion into constant stack space.
The Lycoris Team · · 4 min read Memoization vs. Tabulation in Dynamic Programming
Memoization caches results top-down via recursion; tabulation builds a table bottom-up with loops. Same technique, opposite direction, different tradeoffs.
The Lycoris Team · · 4 min read Manacher's Algorithm Explained
Manacher's algorithm finds the longest palindromic substring in linear time by reusing symmetry from palindromes already found, avoiding redundant checks.