This repository demonstrates how CPU-intensive tasks affect Node.js performance and how to solve the problem using a Worker Pool powered by Node’s worker_threads.
The project includes:
- blocking implementation (server-bad.js)
- non-blocking implementation using a worker pool (server-good.js)
- benchmark harness using autocannon to measure performance under different workloads.
The goal is to show when Node.js performs extremely well and when it can collapse under CPU pressure.
Node.js runs JavaScript on a single-threaded event loop.
This design is extremely efficient for I/O-bound workloads, such as:
-
APIs
-
network services
-
database operations
-
real-time systems
However, CPU-bound tasks can block the event loop.
Examples include:
-
password hashing
-
cryptography
-
image/video processing
-
large JSON parsing
-
complex validation
When CPU work runs synchronously on the main thread:
Incoming requests
↓
Event Loop
↓
CPU-heavy task
↓
All requests wait
The server becomes temporarily frozen until the task finishes.
The file:
src/server-bad.js
simulates a CPU-heavy validation step running directly on the main thread.
Under load this causes:
-
extremely high latency
-
request queueing
-
drastic throughput collapse
-
possible request timeouts
Because Node’s event loop processes tasks serially:
Request A → CPU task
Request B waits
Request C waits
Request D waits
All users are affected by one expensive request.
Node provides the worker_threads module for executing JavaScript on separate threads.
However, spawning a new worker per request is inefficient because thread creation is expensive.
The correct approach is a Worker Pool.
This project uses:
Piscina
Architecture:
HTTP Request
│
▼
Main Event Loop
│
▼
Worker Pool (Piscina)
│
▼
CPU-intensive task
Key advantages:
-
Event loop remains responsive
-
CPU tasks run in parallel
-
workers are reused instead of constantly created
The repository includes a benchmark script using:
Autocannon
The test simulates increasing CPU workloads.
| Scenario | Architecture | Requests/sec | Avg Latency (ms) | p99 Latency (ms) | Total Errors |
|---|---|---|---|---|---|
| Light (10 chars) | Bad Server (Blocking Event Loop) | 738.00 | 13.05 | 30.00 | 0 |
| Light (10 chars) | Good Server (Worker Pool) | 131.10 | 75.30 | 207.00 | 0 |
| Moderate (100 chars) | Bad Server (Blocking Event Loop) | 20.30 | 468.00 | 1824.00 | 0 |
| Moderate (100 chars) | Good Server (Worker Pool) | 97.40 | 101.88 | 1344.00 | 0 |
| Heavy (1k chars) | Bad Server (Blocking Event Loop) | 9.50 | 989.09 | 1958.00 | 0 |
| Heavy (1k chars) | Good Server (Worker Pool) | 44.50 | 221.38 | 498.00 | 0 |
| Collapse (50k chars) | Bad Server (Blocking Event Loop) | 0.00 | 0.00 | 0.00 | 20 |
| Collapse (50k chars) | Good Server (Worker Pool) | 0.60 | 8931.50 | 9598.00 | 8 |
The results demonstrate the performance crossover point:
| Workload Type | Best Architecture |
|---|---|
| Light CPU | Event Loop |
| Moderate CPU | Worker Pool |
| Heavy CPU | Worker Pool |
| Extreme CPU | Worker Pool |
When CPU tasks become expensive, the event loop becomes a bottleneck.
📉 Example Performance Behavior Event Loop Under CPU Load
Request 1: CPU task
Request 2: waiting
Request 3: waiting
Request 4: waiting
Latency grows rapidly.
Worker Pool Execution
Request 1 → Worker 1
Request 2 → Worker 2
Request 3 → Worker 3
Request 4 → Worker 4
Tasks run in parallel.
- Install dependencies
npm install
- Run the automated benchmark
npm run test-load
This command will:
-
start both servers
-
run multiple load scenarios
-
shut down the servers safely
npm run test-load
Example result pattern:
===========================================
SCENARIO: Moderate (100 chars)
===========================================
Starting load test for Bad Server (Blocking Event Loop) with 100 chars payload...
Running 10s test @ http://localhost:3000/test
10 connections
┌─────────┬───────┬────────┬─────────┬─────────┬────────┬───────────┬─────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼───────┼────────┼─────────┼─────────┼────────┼───────────┼─────────┤
│ Latency │ 92 ms │ 125 ms │ 1768 ms │ 1824 ms │ 468 ms │ 458.54 ms │ 1886 ms │
└─────────┴───────┴────────┴─────────┴─────────┴────────┴───────────┴─────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Req/Sec │ 10 │ 10 │ 10 │ 90 │ 20.3 │ 24.14 │ 10 │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Bytes/Sec │ 2.49 kB │ 2.49 kB │ 2.49 kB │ 22.4 kB │ 5.06 kB │ 6.01 kB │ 2.49 kB │
└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
Req/Bytes counts sampled once per second.
# of samples: 10
213 requests in 10.02s, 50.5 kB read
--- Results for Bad Server (Blocking Event Loop) ---
Requests/sec: 20.3
Latency average (ms): 468
Latency p99 (ms): 1824
Errors: 0
Timeouts: 0
Non-2xx Responses: 0
Starting load test for Good Server (Worker Pool) with 100 chars payload...
Running 10s test @ http://localhost:3001/test
10 connections
┌─────────┬───────┬───────┬────────┬─────────┬───────────┬───────────┬─────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼───────┼───────┼────────┼─────────┼───────────┼───────────┼─────────┤
│ Latency │ 12 ms │ 26 ms │ 624 ms │ 1344 ms │ 101.88 ms │ 194.03 ms │ 1553 ms │
└─────────┴───────┴───────┴────────┴─────────┴───────────┴───────────┴─────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Req/Sec │ 6 │ 6 │ 124 │ 153 │ 97.4 │ 59.18 │ 6 │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Bytes/Sec │ 1.58 kB │ 1.58 kB │ 32.8 kB │ 40.4 kB │ 25.7 kB │ 15.6 kB │ 1.58 kB │
└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
Req/Bytes counts sampled once per second.
# of samples: 10
984 requests in 10.04s, 257 kB read
--- Results for Good Server (Worker Pool) ---
Requests/sec: 97.4
Latency average (ms): 101.88
Latency p99 (ms): 1344
Errors: 0
Timeouts: 0
Non-2xx Responses: 0
The blocking server experiences queueing and latency spikes, while the worker pool remains stable.
Use worker threads when tasks are CPU-bound:
-
password hashing (bcrypt / argon2)
-
cryptography
-
compression
-
image/video processing
-
large schema validation
-
machine learning inference
Do not use workers for normal API logic or I/O.
Node's event loop is already optimal for that.
This repository demonstrates a specific architectural limitation, not a flaw in Node.js itself.
In fact, Node.js is used at massive scale by companies like:
-
Netflix
-
PayPal
-
Uber
Because it excels at high-concurrency I/O workloads.
Worker pools simply complement the event loop for CPU work.
src/
├── server-bad.js
│ Blocking CPU task on event loop
│
├── server-good.js
│ Worker pool using Piscina
│
├── worker.js
│ CPU intensive task executed in workers
│
├── load-test.js
│ Benchmark runner
│
├── examples/
│ ├── 1-password-hashing.js
│ ├── 2-cryptography.js
│ ├── 3-image-processing.js
│ ├── 4-large-json-parsing.js
│ └── 5-complex-validation.js
└── reports/
We have included standalone, heavily-commented test examples of common Event Loop blockers in the src/examples directory. Each file simulates one of the core CPU-heavy operations and clocks exactly how many milliseconds the main thread is frozen for.
You can run our automated graphical test suite which executes all 5 bottlenecks and plots their freezing times on a generated HTML dashboard:
npm run test-examplesOr you can run them individually in the terminal to learn about the specific bottlenecks:
node src/examples/1-password-hashing.js
node src/examples/2-cryptography.js
node src/examples/3-image-processing.js
node src/examples/4-large-json-parsing.js
node src/examples/5-complex-validation.jsTry modifying the workload in worker.js and observe the performance curve.
Interesting experiments:
-
increase CPU workload
-
increase concurrency
-
reduce worker pool size
-
simulate real validation workloads