Learn / Performance / Lesson 14
CPU and the event loop
Node.js runs your code on one thread. One slow regular expression can stop every user at once, and it has taken down two of the biggest sites on the web.
Last updated: 2026-09-19
What it is
Node.js runs JavaScript on a single thread with an event loop: it picks up the next piece of work, runs it to the end, then picks up the next. Waiting on the network or the disk is free, because Node does other work meanwhile. But while your code is computing, nothing else runs. Not other requests, not timers, not health checks.
So in Node, a CPU problem for one request is a CPU problem for everyone.
Why it is a rule
- Stack Overflow, 20 July 2016. A post contained about 20,000 spaces in a row. The regular expression that trimmed whitespace from the ends of lines backtracked over them, and the work grew with the square of the length. The home page, which showed that post, took the site down for 34 minutes.
- Cloudflare, 2 July 2019. A new firewall rule contained a regular expression that backtracked catastrophically. It was pushed to every server at once, and CPU went to 100% worldwide. For 27 minutes, a large share of the web answered with errors.
Both were a single pattern, written by capable engineers, that was fast on normal input and explosive on unusual input.
How to do it
Treat regular expressions as code that can explode
Nested or overlapping repeats like (a+)+, (.) or \s+$ on untrusted text can take exponential or quadratic time.
/\s+$/.test(untrusted)
untrusted.trimEnd()The first line slows down with the square of the input on a long run of spaces followed by one letter, because the engine retries the match from every space. The second is one pass.
- Prefer string methods and simple loops for trimming, splitting and searching.
- Cap the length of any text before a pattern sees it.
- Test patterns against long, hostile input, not only against the happy case.
Pick the right algorithm first
Most speed comes from doing less, not doing it faster.
- A lookup inside a loop over the same list is O(n²). Build a
Maponce and it becomes O(n). - Sorting to find one maximum is wasted work. One pass finds it.
- The output must be identical after the change. Speed that changes the answer is a bug.
Keep big work off the request path
- Stream large responses (exports, reports) instead of building them whole in memory.
- Page big lists; never load every row to show twenty.
- Move truly heavy computation to a worker thread or a background job, so requests keep flowing.
Watch the event loop
perf_hooks.monitorEventLoopDelay() reports how late the loop is running. A rising delay means something is holding the thread.
How we do it here
Text parsers work line by line with anchored patterns and cap their input sizes. Large downloads such as ledgers and spreadsheets are streamed, not built whole in memory. Work whose inputs have not changed is done once and reused.
Benefits
- One heavy request no longer freezes every other user.
- Linear algorithms stay fast as a client's data grows from hundreds of rows to millions.
- Lower CPU means a smaller, cheaper server does the job.
Disadvantages
- Streaming and paging are more code than "load it all and loop".
- Worker threads add message passing and a second place for bugs to hide.
- Replacing a clever regular expression with a loop is often longer and less obvious to read.
Checklist
- No backtracking-prone pattern runs on untrusted, unbounded input.
- No O(n²) loop over data that grows with the client.
- Big results are streamed or paged.
- Event-loop delay is measurable.
Sources
- Node.js — Don't block the event loop
- Cloudflare — Details of the Cloudflare outage on July 2, 2019
- OWASP — Regular expression Denial of Service
- Stack Exchange, Outage Postmortem — July 20, 2016.