Skip to content
rusthacks.dev
CLI

Regex in Rust the regex Crate and Its Guarantees Explained

The regex crate guarantees linear time matching by leaving out backreferences and lookaround. See the API, the compile-once rule and what to use when you need the missing features.

3 min read
Summarize in AI
Flat illustration for the post “Regex in Rust the regex Crate and Its Guarantees Explained”, composed around cargo.

Regex in Rust looks like regex anywhere, with two differences that matter: it is compiled once into an automaton, and it will never take exponential time. The price is a missing feature set. This post covers the API you use daily, the compile-once rule, and what to do when the syntax you want is not there. It is a core tool for the command line tool series, for example in a log analyzer.

regex 1, Rust 1.90.

Match and capture

rust
use regex::Regex;
use std::sync::LazyLock;

static DATE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})").unwrap());

fn main() {
    let text = "released 2025-09-18, patched 2025-10-02";
    println!("{}", DATE.is_match(text));

    if let Some(caps) = DATE.captures(text) {
        println!("{} {} {}", &caps["year"], &caps["month"], &caps["day"]);
    }

    let all: Vec<&str> = DATE.find_iter(text).map(|m| m.as_str()).collect();
    println!("{all:?}");

    println!("{}", DATE.replace_all(text, "$day/$month/$year"));
}

It prints true, then 2025 09 18, then ["2025-09-18", "2025-10-02"], then released 18/09/2025, patched 02/10/2025. The r"..." raw string avoids doubling backslashes. The unwrap on a literal pattern is fine: an invalid literal is a bug caught the first time the program runs.

Compile once

Regex::new parses and compiles the pattern, which can cost microseconds to milliseconds depending on size. A Regex::new inside a loop that handles a million lines compiles a million times. Put it in a LazyLock static as above, or in a struct field built at startup.

The guarantee and its cost

The engine is an automaton, so matching time grows linearly with the input. A pattern like ^(a+)+$ that makes a backtracking engine hang on aaaa...b is harmless here. That makes it safe to run user supplied patterns with a size limit (RegexBuilder::size_limit). The limitation: no backreferences (a repeated group such as a doubled letter) and no lookaround ((?=...)). The compile error looks like regex parse error: look-around, including look-ahead and look-behind, is not supported.

When you need the missing features

NeedOption
Lookaround, backreferencesfancy-regex, which backtracks and can be slow
Many patterns at onceregex::RegexSet or aho-corasick for literals
Non UTF-8 inputregex::bytes::Regex
PCRE compatibilitypcre2 crate, with C dependencies

If a lookahead was only there to assert context, a second pass over the captures usually replaces it.

Unicode

The digit class matches any Unicode decimal digit, the word class any Unicode word character, and . any scalar value except a newline. For ASCII only, write [0-9] or turn Unicode off with (?-u). Case insensitive (?i) follows Unicode case folding.

Performance

Searching literals is optimized, and anchored patterns are cheap. For simple substring tests, str::contains is faster than a regex. For heavy loads, read the performance notes in the crate docs, and measure before tuning.

Where regex is wrong

Parsing nested structures like HTML or JSON; use a parser, or serde_json as in JSON with Serde. The API is documented in the regex crate docs, including the syntax reference.

Taggedregex
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.