Regex is not usually a line item on anyone's cloud bill, but it hides inside two that are: log ingestion and CPU seconds. A badly written pattern in a log agent can push gigabytes a day into a vendor that charges per GB, and a badly written pattern in a request path can pin a core. Both are fixable with edits measured in characters.
Observability vendors bill on ingest. Every health-check line your load balancer emits costs the same as a real error. Dropping them at the collector is the single largest saving available. In a Fluent Bit or Vector config, an exclusion rule such as ^(?:GET|HEAD) /(?:healthz|readyz|favicon\.ico)\b typically removes 20–40 percent of raw HTTP access-log volume on a service behind Kubernetes probes, because probes fire every few seconds per pod and real traffic does not.
Do the arithmetic before you argue about it: 40 GB/day at a common $0.10–$0.50 per GB ingest price is $120–$600 a month for lines nobody has ever read.
An unanchored pattern makes the engine retry at every position in the subject string. error against a 4 KB log line attempts up to 4,000 starting offsets. ^\S+ \S+ error attempts one. Anchoring with ^, or using a literal prefix the engine can use to skip ahead, is usually a 10× or better improvement on long lines and costs nothing.
The classic shape is nested quantifiers over overlapping character classes:
# Exponential: try it on "aaaaaaaaaaaaaaaaaaaaaaaaaaX"
^(a+)+$
# Same intent, linear
^a+$
# Email-ish validator that blows up on long invalid input
^([a-zA-Z0-9_\.\-]+)*@
# Fixed: one quantifier, no nesting
^[a-zA-Z0-9_.\-]+@
The failure mode is not gradual. A pattern that runs in microseconds on 20 characters can run for minutes on 40, because each added character doubles the work. That is a denial-of-service vector any time user input reaches the regex, and on serverless platforms billed per GB-second it is a direct charge.
In PCRE, Java and Ruby you can tell the engine never to give characters back: \d++ instead of \d+, or (?>[^,]+) instead of ([^,]+). Once a possessive quantifier has consumed, it will not backtrack into that span, which collapses the search space. Python's re gained possessive quantifiers and atomic groups in 3.11; before that, the standard workaround was a lookahead capture, (?=(\d+))\1.
Go's regexp, Rust's regex crate, and Google's RE2 use a finite-automaton approach with linear time guarantees. They deliberately drop backreferences and lookaround, and in exchange no input can make them hang. If you are running user-supplied patterns — a search box, a log query UI, an alert rule builder — that trade is worth making. Node users can get the same guarantee through the re2 package.
A surprising share of production regexes are doing work that startswith, split or a substring check would do faster and more readably. re.match(r'^https?://', url) is slower and harder to read than url.startswith(('http://', 'https://')). Compile the patterns you keep once at module load rather than inside the loop; in Python the cache is limited to 512 entries and dynamically built patterns evict it.
Use regex101.com's debugger to count steps — it shows a step count per match attempt, and anything in the tens of thousands for a short input is a red flag. For a real workload, python -m timeit or a flame graph filtered to your regex module tells you whether the pattern is actually where the time goes before you spend an afternoon on it.