Lookaround isn't free
Each lookaround is a sub-pattern that the engine evaluates independently at every relevant position. Patterns with multiple lookarounds can be slower than equivalent capture-and-test patterns, especially on long inputs.
When the engine has to backtrack
Lookarounds combined with greedy quantifiers can multiply backtracking. (?=.*X)(?=.*Y).* against an input that ALMOST has X and Y will trigger a lot of work as the engine confirms each lookahead and then tries the main pattern.
Mitigation: anchor your lookarounds. ^(?=.*X)(?=.*Y).*$ at least limits scope to the whole string, not every position.
When NOT to use lookaround
1. When a character class works. (?=[a-z]) at the start of a pattern is just "matches a lowercase letter." Use [a-z] directly unless you need to NOT consume.
2. When two-pass is clearer. Match broadly with a regex, then filter in code. Often more readable than a single mega-pattern with stacked lookarounds.
3. When the engine doesn't support it. Go, RE2, ripgrep default. Use captures.
The first-pass test
Before reaching for lookaround, ask: "Can I solve this with a capture group + .group(1) or by post-processing?" The capture solution is more portable and often clearer. Reach for lookaround when the value of "don't consume" is high — for splits, for stacked rules, for sed-friendly patterns.
Pippa's lookaround in real code
Search the cwkPippa codebase for (?= and (?<=. You'll find them in route matching, log parsing, and Markdown sanitation — but they're outnumbered ~10:1 by capture groups. The right tool depends on the job.