Regex engines compared: JavaScript, Python, Java, PCRE and RE2
Which regex features work in which engine, tested side by side: lookbehind, named groups, Unicode, atomic groups, recursion, what $ does before a newline, and which engines can freeze on a bad pattern.

· 6 min read
/\$\s*(\d+)/
match
- $40
- $ 40
- $5
spare
- 40
- USD 40
- $x
JavaScript, Python, Java and PCRE are all backtracking regex engines with nearly the same core syntax, so most everyday patterns work in all four. They differ in the details that break code when you move a pattern: how much lookbehind they allow, how named groups are written, whether \d matches non-English digits, and what $ does before a trailing newline. RE2 (used by Go, Google Sheets and BigQuery) is the odd one out: it drops backreferences and lookarounds in exchange for a guarantee that matching never takes more than linear time.
Every row below was tested on 4 November 2026, not copied from documentation: Node 22.23 for JavaScript, Python 3.12.6 (re), Java 17.0.20 (java.util.regex), PCRE2 10.44 through PHP 8.5, and RE2 through Google's google-re2 1.1 package. Newer versions may support more.
01Where you'll meet each engine
- JavaScript: every browser, Node, Deno and TypeScript. It's also the engine behind the boards on this site.
- Python: the built-in
remodule. The third-partyregexmodule adds a lot of whatrelacks. - Java:
java.util.regex, which Kotlin and Scala use on the JVM. - PCRE2: PHP's
preg_functions,grep -P, nginx, and R withperl = TRUE. - RE2: Google's library. Go's
regexppackage follows its syntax and its speed guarantee, and so do Google Sheets'REGEXMATCHand BigQuery'sREGEXP_CONTAINS.
02The feature table
| JavaScript | Python re |
Java | PCRE2 | RE2 | |
|---|---|---|---|---|---|
Lookahead (?=…) |
Yes | Yes | Yes | Yes | No |
Lookbehind (?<=…) |
Yes, any length | Fixed width only | Yes, bounded length¹ | Yes, bounded length | No |
Backreferences \1 |
Yes | Yes | Yes | Yes | No |
| Named group | (?<n>…) |
(?P<n>…) |
(?<n>…) |
Both | Both |
| Named backreference | \k<n> |
(?P=n) |
\k<n> |
Both | No |
Atomic group (?>…) |
No | Yes (3.11+) | Yes | Yes | No |
Possessive a++ |
No | Yes (3.11+) | Yes | Yes | No |
Unicode property \p{L} |
Yes, with the u flag |
No | Yes | Yes | Yes |
| Script, e.g. Greek | \p{Script=Greek} |
No | \p{IsGreek} |
\p{Greek} |
\p{Greek} |
\d matches ٣ (Arabic-Indic 3) |
No | Yes | No | Only in UCP mode² | No |
Recursion (?R) |
No | No | No | Yes | No |
Conditional (?(1)…|…) |
No | Yes | No | Yes | No |
Inline flags (?i) |
No³ | Yes | Yes | Yes | Yes |
^abc$ matches "abc\n" |
No | Yes | Yes | Yes | No |
End of text \z |
No | No, \Z means it |
Yes | Yes | Yes |
| Can freeze on a bad pattern | Yes | Yes | Yes | Stops at a limit⁴ | No |
- Java's documentation asks for a bounded maximum length, but Java 17 also accepted
(?<=a+)in our test. Don't rely on it. - PCRE2's default
\dis ASCII. PHP's/uflag turns on UCP mode, which is why\dmatched٣there. - Node 22 rejects both
(?i)and the scoped(?i:…). The scoped form is new in ES2025 and only works in newer runtimes. - See the speed test below: PHP returned an error instead of an answer.
03The differences that break moved patterns
$ and a trailing newline
^abc$ matches "abc\n" in Python, Java and PCRE, because their $ also matches just before a final newline. In JavaScript and RE2 it doesn't. A line read from a file with its newline still attached passes validation in one language and fails in another. To mean "the very end", use \z in Java, PCRE and RE2, and \Z in Python (where \Z is the absolute end). JavaScript has neither: without the m flag, its $ already means the very end.
Lookbehind
JavaScript allows any lookbehind, including variable-length ones like (?<=\$\s*)\d+. Python's re only allows fixed width, PCRE2 a bounded length, and RE2 none at all. The portable fix is to match the prefix and capture what you want instead:
/\$\s*(\d+)/Should match2/2
- Total: $40
- Total: $ 40
Should not2/2
- Total: 40
- Total: USD 40
Group 1 holds the number in every engine, RE2 included.
Named groups
(?<year>\d{4}) works in JavaScript, Java, PCRE2 and RE2, but Python 3.12's re rejects it and wants (?P<year>\d{4}). Backreferences to a name differ too: \k<year> in JavaScript, Java and PCRE, (?P=year) in Python. JavaScript vs Python regex has the full list for those two.
\d, \w and Unicode
In Python 3, \d and \w match digits and letters from every script by default, so \d accepts ٣. In JavaScript, Java and RE2 they're ASCII only. For a form field that must be 0-9, write [0-9] and the pattern means the same thing everywhere (in Python you can also pass re.ASCII).
For letters in any language, \p{L} works in JavaScript (with the u flag), Java, PCRE2 and RE2, but not in Python's re.
Atomic groups and possessive quantifiers
(?>a+) and a++ take as much as they can and never give it back, which is one way to stop runaway backtracking. Java and PCRE have had them for years, and Python added them in 3.11. JavaScript and RE2 don't have them; in JavaScript, a lookahead with a backreference, (?=(a+))\1, does the same job.
04Speed: which engines can freeze
The classic bad pattern is (a+)+$ against a long run of the letter a followed by !. A backtracking engine tries every way to split the run between the inner and outer + before it gives up. Here is what each engine did with 24 letters and a !:
| Engine | Result |
|---|---|
| JavaScript (Node 22) | 11.2 seconds |
| Python 3.12 | 1.8 seconds |
| Java 17 | Under 1 ms |
| PCRE2 (PHP) | Gave up after 3 ms with "Backtrack limit exhausted" |
| RE2 | Under 1 ms, even with 100,000 letters |
Two things stand out. Java 17 optimizes this shape, but it's still a backtracking engine: add a backreference, as in ^(a+)+\1$, and its time went from 9 ms to 45 ms to 590 ms as the input grew from 16 to 20 to 24 letters. PHP doesn't hang, but preg_match returns false, and code that only checks for a truthy match reads that as "no match" without noticing the error.
Only RE2 guarantees linear time for every pattern, which is why it's the safe choice for patterns users type in. Catastrophic backtracking explains how to spot and fix these patterns in any engine.
05Writing a pattern that works everywhere
If a pattern has to run in more than one engine, for example on the browser and on the server, this subset behaves the same in all five:
- Literal text, classes like
[a-z0-9_],., and the quantifiers* + ? {n,m}with their lazy versions. - Anchors
^and$with themflag when you mean lines, or\A…\zon the server when you mean the whole text (and never feed a trailing newline to a JavaScript$). - Groups
(…)and(?:…), and alternation|. [0-9]rather than\d, if non-English digits must be rejected.- No lookarounds or backreferences if RE2 might run it; capture the part you want instead.
Then test the pattern in each engine it will run in. The Playground runs JavaScript's engine, the same as this page.
06Practice
The practice packs and the daily hunt use JavaScript's engine, so lookbehind and named groups are fair game there. The Regex Guide covers each feature with live examples.