How We Detected a ReDoS Vulnerability in Our Regex Before Production Deployment
A detailed account of how load testing exposed a catastrophic backtracking flaw in our input validation logic and how we neutralized it without compromising user experience.


It was a Tuesday afternoon in March 2026 when our staging environment finally surrendered. We were three days away from launching the "Collaborative Workspaces" feature, a complex addition allowing users to generate sharable project identifiers. The feature seemed trivial on the surface: an input field accepting a string that we would validate, sanitize, and store.
During the final round of integration testing, our DevOps engineer, Sarah, ran a standard load simulation using k6. She aimed for a moderate load of 200 requests per second. Within thirty seconds, the monitoring dashboard lit up red. The Node.js API response times jumped from an average of 40ms to over 12 seconds, and the CPU utilization on our container instances pinned itself at 100%. The server hadn't crashed; it was paralyzed, stuck in an infinite computation loop that ate every cycle available.
We initially suspected a memory leak in the new database connection pool or perhaps an unhandled promise rejection. But profiling the heap revealed nothing unusual. The memory usage was stable. The issue was strictly CPU-bound.
The Load Test That Broke the Camel’s Back
The panic set in when we realized the "hanging" requests weren't timing out; they were just stalling the event loop entirely. Because Node.js is single-threaded, a CPU-heavy task blocks everything. Incoming SSL handshakes, health checks, and even static file serving halted. The whole cluster became unresponsive.
I isolated the specific endpoint causing the blockage: POST /api/v1/workspaces/validate. This endpoint took a user-proposed slug and checked if it met our naming conventions. I duplicated the regex logic into a standalone script and ran it against the payload generated by the load test.
The culprit was a regular expression designed to ensure the slug contained alphanumeric characters, hyphens, or underscores, but didn't start or end with a separator. The pattern looked innocent enough:
const workspaceSlugRegex = /^([a-zA-Z0-9]+[-_]?)+$/;
The intent was clear: match one or more alphanumeric characters, optionally followed by a separator, and repeat that group. It works perfectly for valid strings like "my-project-2026". However, when Sarah's load script hit the API with an edge case—a long string of alphanumeric characters ending with a character that didn't fit the expected termination pattern—the regex engine lost its mind.
Specifically, a string like aaaaaaaaaaaaaaaaaaaaX caused the engine to work exponentially. The regex tries to match the [a-zA-Z0-9]+ group in various ways, backtracking repeatedly to see if a different division of characters would satisfy the [-_]? condition and the outer + quantifier. We were witnessing catastrophic backtracking.
Why the Engine Choked
To understand the failure, you have to step into the shoes of the regex engine. When it encounters the quantifiers + inside +, it enters a combinatorial nightmare.
Imagine the engine processing the string aaaaX. It matches the first four as with [a-zA-Z0-9]+. It moves to [-_]? and finds nothing, so it proceeds to the end of the group. The outer + says "do it again." But we are at X. The inner + fails. The engine backtracks.
It thinks: "Maybe I shouldn't have consumed all four as with the inner +." So it tries matching three as, checks the separator (fails), tries the outer group again, and fails. It tries two as. It tries one a. For a string of length 20, the engine performs millions of calculations to determine that the string does not match.
This is the core of a ReDoS (Regular Expression Denial of Service) attack. An attacker doesn't need a botnet; they just need to send a single, carefully crafted 30-character string to your validation endpoint, and your server hangs.

The image above illustrates the "time complexity" explosion. While a linear regex stays flat regardless of input length, our vulnerable pattern spiked vertically. In production, this would have meant a single malicious user could take down the API for all our customers.
Atomic Groups to the Rescue
Fixing the issue required changing how the engine "commits" to a match. We needed to tell the regex engine: "If you match the alphanumeric characters, do not give them back to try and find a separator later."
Different regex engines handle this differently. In JavaScript, which relies on the V8 engine (in Chrome/Node) or SpiderMonkey (in Firefox), full support for "possessive quantifiers" (like ++) is historically inconsistent or non-existent in older environments, though modern engines are improving. However, as of 2026, we can rely on a more robust approach using capturing groups and lookahead assertions, or simply refactoring the logic to remove nested quantifiers.
I chose the path of least resistance and highest performance: refactoring to avoid the nested repetition entirely. Instead of saying "groups of characters separated by optional separators," I flipped the logic to be explicit about separators.
The new pattern became:
const safeSlugRegex = /^[a-zA-Z0-9]+(?:[-_][a-zA-Z0-9]+)*$/;
This reads as: "Start with alphanumeric characters. Then, zero or more times, match a separator followed immediately by alphanumeric characters."
This structure is linear. The engine never has to backtrack into the initial alphanumeric set to satisfy a separator because the separator is a required prefix for any subsequent repetition. Running the same malicious payload aaaaaaaaaaaaaaaaaaaaX against this new pattern resulted in an instantaneous failure—milliseconds instead of minutes.
This change is critical for web-security because it shifts the complexity from polynomial time O(2^n) to linear time O(n). A user can send a 10,000-character string, and it will simply fail fast without burning CPU cycles.
Shifting Left the Regex Defense
The fix was deployed, and the load test passed with flying colors. But the experience forced us to acknowledge a gap in our development lifecycle. We had unit tests for valid inputs, but we never tested for "pathological" inputs.
To prevent this from recurring, we integrated a linting rule into our ESLint configuration using the eslint-plugin-security (or a similar specialized plugin). We also looked into tools like safe-regex, which analyzes patterns statically to detect potential exponential backtracking risks.
However, static tools aren't perfect. They generate false positives, flagging complex but safe patterns. The real shift had to be cultural. We now treat regex patterns with the same caution we treat SQL queries. We validate them not just on whether they match the correct data, but on how they behave when they don't match.
Furthermore, we implemented a timeout wrapper for any server-side regex execution. Using Node.js, we can run risky regex operations in a separate worker thread or simply wrap the execution in a logical check that aborts if the backtracking steps exceed a certain threshold. While JavaScript doesn't natively support a "timeout" parameter in the .test() or .exec() methods, moving the validation to a Web Worker (in the browser) or a Worker Thread (in Node.js) ensures that a catastrophic regex failure only crashes that specific thread, leaving the main event loop free to handle requests and respond with a 400 Bad Request error.
It is worth noting that while we focused on the server-side impact, ReDoS is also a browser concern. If your application uses complex regexes on the client side for progressive enhancement or large text parsing, a malicious script on the page (or a crafted clipboard paste) can freeze the user's tab, violating accessibility guidelines regarding responsiveness. We have since moved all heavy text processing off the main thread in our frontend codebase to adhere to these standards.
Runtime Consistency in 2026
One caveat I must mention is browser compatibility. In the past, we avoided complex lookaheads because of spotty support in Safari or older Internet Explorer versions. Today, the landscape is much more uniform. All major modern browsers—including Chrome 120+, Firefox 130+, and Safari 17+—support the Unicode property escapes and advanced lookbehinds we might need for robust validation.
However, the specific fix we applied—removing nested quantifiers—has the distinct advantage of being compatible with regex engines from the 1990s. We didn't need to use modern syntax to solve the problem; we needed better logic. This ensures that if our backend validation logic is ever ported to a legacy system or a different language (like Go or Python for a microservice), the safety travels with the code.
When we secure our stack, we often think about headers to prevent XSS and clickjacking. We configure Content Security Policy and strict transport security. But we frequently overlook the input validation layer as an attack surface. A ReDoS vulnerability is effectively a self-inflicted Denial of Service.
Validation is a Feature, Not a Check
The outage on that Tuesday was a blessing in disguise. If we had shipped that feature to production, the first person to encounter a bug and type a frustrated string of aaaaa... into the support chat field or workspace generator would have triggered an incident that could have cost us hours of uptime and trust.
We learned that input validation isn't just a gatekeeper for data integrity; it's a performance bottleneck waiting to happen. Every regex you write must be profiled against the "worst-case scenario." It is not enough that it accepts the good; it must reject the bad efficiently.
As we continue to build more interactive and complex web applications, the reliance on pattern matching will only grow. Whether we are parsing JWTs versus session cookies for authentication or scrubbing user content for profanity, the regex engine remains a powerful tool. It respects those who understand its internal mechanics and punishes those who treat it like magic.
The next time you copy a regex from Stack Overflow or ask an LLM to generate a pattern for email validation, pause. Ask yourself what happens when that pattern faces a string designed to break it. Your server's CPU will thank you.

