Skip to content

CozyRunner/node-eventloop-test

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Node.js Event Loop: Blocking vs Non-Blocking (Worker Pools)

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.

⚡ Background

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 Problem: Blocking the Event Loop (src/server-bad.js)

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.

✅ The Solution: Worker Pools (src/server-good.js)

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

📊 Benchmark Results

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

Key Insight

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.

🚀 Running the Benchmark

  1. Install dependencies
npm install
  1. 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 CLI Output (Sample)

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.

🧠 When Should You Use Worker Pools?

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.

⚠️ Important Note

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.

📂 Project Structure

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/

🧪 Runnable Test Examples

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-examples

Or 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.js

🧪 Recommended Experiments

Try 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

📜 License

MIT

About

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.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors