Sitemap

The Dark Side of Node.js: How CPU-Intensive Tasks Can Kill Your API Performance (and How to Fix It!)

5 min readApr 1, 2025

--

Press enter or click to view image in full size

Node.js is widely praised for its asynchronous, event-driven architecture, making it a top choice for building fast and scalable applications. But what happens when your app encounters a CPU-intensive task?

Imagine this: You built a Prime Number API where users send a number, and your server responds with whether it’s prime or not. Suddenly, one million users flood your server with requests. What happens next? Your Node.js app slows down, responses take forever, and the entire API freezes up.

Why? Because CPU-bound tasks like prime number checking don’t play well with Node.js’s single-threaded event loop. But don’t worry. In this post, we’ll explore:

  • Why CPU-heavy tasks block the event loop
  • The hidden performance bottleneck that can crash your app
  • Powerful solutions to handle heavy computations efficiently

How CPU-Intensive Tasks Break Node.js

Node.js is built on the single-threaded event loop, which means it handles only one request at a time. This works perfectly for I/O operations like database queries or HTTP requests, but not for CPU-bound tasks like complex mathematical computations.

What Happens When You Process a CPU-Intensive Task?

A user sends a request to check if 987654323 is prime. Your server starts the computation, and everything else stops.

The event loop is busy crunching numbers and can’t process any other requests until it finishes. This means:

  • Request 1: Prime checking starts and takes three seconds
  • Request 2: Must wait until Request 1 finishes
  • Request 3: Must wait until Request 2 finishes
  • Request 1,000,000: Stuck in a queue, waiting for their turn

The Performance Disaster: One Million Requests, One CPU Core

If you receive one million requests, your API won’t scale because it’s processing one request at a time. In contrast, a database query (an I/O operation) is non-blocking, allowing other requests to be handled simultaneously.

How to Handle CPU-Intensive Tasks in Node.js

To avoid blocking the event loop and slowing down your API, here are three solutions you can implement:

1. Offload Heavy Computation to Worker Threads

Best for CPU-intensive tasks like prime number checking, image processing, or encryption

Worker Threads allow Node.js to run CPU-heavy tasks in parallel on separate threads without blocking the event loop.

Implementing Worker Threads

i) index.js

This file sets up an Express-based API and uses a pool of worker threads to handle CPU-bound tasks like checking if a number is prime.

const express = require('express');
const { Worker } = require('worker_threads');
const os = require('os');
const app = express();

// Number of worker threads to spawn based on available CPU cores
const numThreads = os.cpus().length;
const workerPool = [];

// A queue to manage pending requests
const requestQueue = [];

// Create worker threads
for (let i = 0; i < numThreads; i++) {
const worker = new Worker('./worker.js');
worker.isIdle = true;
workerPool.push(worker);

// Handle messages from worker (result of prime checking)
worker.on('message', (result) => {
console.log(`Worker Result: ${result}`);
worker.isIdle = true;

// Process the next request in the queue
if (requestQueue.length > 0) {
const nextRequest = requestQueue.shift();
worker.isIdle = false;
worker.postMessage(nextRequest);
}
});

// Handle errors from worker
worker.on('error', (error) => {
console.error(`Worker error: ${error}`);
});

// Handle worker exit
worker.on('exit', (code) => {
if (code !== 0) {
console.error(`Worker stopped with exit code ${code}`);
}
});
}

// Middleware to parse JSON requests
app.use(express.json());

// API route to check if a number is prime
app.post('/is-prime', (req, res) => {
const { number } = req.body;

if (typeof number !== 'number') {
return res.status(400).json({ error: 'Please provide a valid number' });
}

// Find an idle worker or queue the request if all are busy
const idleWorker = workerPool.find((worker) => worker.isIdle);

if (idleWorker) {
idleWorker.isIdle = false;
idleWorker.postMessage(number);
res.status(202).json({ message: 'Processing prime check request' });
} else {
requestQueue.push(number); // Add request to the queue if no worker is available
res.status(202).json({ message: 'Queued for processing' });
}
});

// Start the server
app.listen(3000, () => {
console.log('Server is running on http://localhost:3000');
});

ii) worker.js (Worker Thread)

This script contains the logic for checking if a number is prime. It runs in a separate thread and performs the computation without blocking the main event loop.

const { parentPort } = require('worker_threads');

// Function to check if a number is prime
function isPrime(n) {
if (n <= 1) return false;
if (n === 2) return true;
if (n % 2 === 0) return false;
for (let i = 3; i <= Math.sqrt(n); i += 2) {
if (n % i === 0) return false;
}
return true;
}
// Listen for messages from the main thread
parentPort.on('message', (number) => {
const result = isPrime(number);
parentPort.postMessage(`Number ${number} is ${result ? 'Prime' : 'Not Prime'}`);
})

Why it Works:

  • Worker threads run in parallel, keeping the main event loop free
  • Each request is handled in its own thread
  • Significant performance boost

2. Use Clustering to Distribute Load

Best for scaling your Node.js app across multiple CPU cores

By default, Node.js runs on a single CPU core, meaning it can’t utilize modern multi-core processors. Clustering allows your app to spawn multiple processes, distributing the load.

Implementing Clustering

Modify your server.js file:

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
});
} else {
http.createServer((req, res) => {
res.writeHead(200);
res.end('Prime Checking API');
}).listen(8000);
}

Why it Works:

  • Each worker runs on a separate CPU core
  • Multiple requests are processed simultaneously
  • Improved scalability without significant code changes

Final Thoughts: Don’t Let CPU Tasks Kill Your API

Key Takeaways

  • Node.js event loop struggles with CPU-heavy tasks
  • One million requests can block your server if not optimized
  • Worker Threads allow CPU tasks to run in parallel
  • Clustering utilizes multiple CPU cores for better scalability
  • Optimized algorithms reduce execution time

If your API processes CPU-bound tasks, these techniques can prevent slowdowns and crashes.

Before launching your next high-traffic API, ask yourself: Am I handling CPU-intensive tasks the right way?

--

--

Aditya Yadav
Aditya Yadav

Written by Aditya Yadav

Software Engineer who talks about tech concepts in web development https://www.linkedin.com/in/aditya-yadav-01/