Multithreading in NodeJS — using worker threads

Subhanu
Engineering at Bajaj Health
7 min readFeb 20, 2024

Whether you are

  • a seasoned pro using NodeJS to build a scalable backend which involves some complex tasks like processing images, uploading PDFs, encryption, etc, or
  • someone who is shifting from a multithreaded language like Java or Go to NodeJS, or
  • a complete beginner learning backend in Javascript

this article will help you understand what NodeJS is, its limitations, what multithreading is, and how to get started with Multithreading in NodeJS.

So let’s dive right in!

What is NodeJS?

NodeJS is a server-side Javascript run time built on the V8 JS engine which has gained immense popularity in the development community because of its

  • event-driven,
  • non-blocking input-output model

which makes it well-suited for building i/o based scalable and high-performance applications

Inner working of NodeJs — GeeksForGeeks

What are its limitations?

NodeJs is single threaded which makes handling multiple IO-bound requests easy.

But when there are CPU-intensive tasks like converting an html to a pdf, compression of documents, image processing or doing a complex calculation, NodeJs is not at all efficient.

Some cons of the single-threaded nature of NodeJs are:

Blocking Operations

Any blocking operation can potentially stall the entire application in a single-threaded environment. CPU-bound tasks or operations with significant processing time can lead to delays in handling other events.

Example 1 — Hashing elements in a large array

We have to perform CPU-intensive tasks like hashing every element in a vast array using the crypto module

Hashing all elements of a large array

This block of code takes a lot of computational time.

Since Node.js runs callbacks registered for events in the Event Loop, this callback code will block the Event Loop thread and cannot handle requests from other clients until it finishes its execution.

Example 2 — Calculating the nth Fibonacci number (n>35)

nth Fibonacci Number

In this example, we have a simple recursive function to calculate the Fibonacci sequence. The recursive nature of the function makes it computationally expensive. When running this function with a large value of n in a single-threaded Node.js environment, it can lead to a significant delay in processing other tasks. The event loop gets blocked, and the application becomes less responsive.

Example 3 — Image Processing

Resizing and grayscaling an image using sharp

In this example, we use the sharp library to perform image processing tasks such as resizing and converting to grayscale. Image processing, especially with large files, can be CPU-intensive. When executing this code in a single-threaded environment, it may take a considerable amount of time to process the image, impacting the responsiveness of the application for other tasks.

Limited CPU Utilization

Node.js cannot fully utilize multi-core processors due to its single-threaded architecture. In scenarios where parallel processing could significantly improve performance, the single-threaded model falls short.

Cons of single-threaded NodeJs: Limited CPU Utilization

Scalability Challenges

Scaling vertically (adding more resources to a single server) can only go so far. Horizontal scaling (adding more servers) is often preferred, but Node.js, in its native form, may not fully leverage the potential of multi-core servers.

Vertical scaling on the left, horizontal scaling on the right

Multithreading vs Single-Threaded Languages

In the simplest of terms,

Single-threaded languages can only execute one task at a time, while multi-threaded languages can execute multiple tasks simultaneously.

Single-threaded vs Multithreaded

Single-threaded languages are straightforward and suitable for simple applications. They are also easy to debug and maintain because of their sequential nature.

Multi-threaded languages can potentially improve performance and responsiveness. For example, Apache servers use a multi-threading mechanism to handle multiple requests simultaneously.

Ok so now we have understood theoretically, let's look at it practically.

Performance testing single-threaded NodeJS

We will create a simple node-express server with 2 routes:

  • “/” route: will be a simple non-blocking operation
  • “/blocking” route: will perform the calculation of the 45th Fibonacci number

Then we will check the response times for 3 scenarios:

  1. Send a request to “/”
  2. Send a request to “blocking”
  3. Send request to “/” while “/blocking” is in progress

Setup Project

Go to any directory of your choice and run the below command in the terminal:

npm init

Index.js file

import express from "express";


const app = express()
const port = process.env.PORT || 5000;

// Making 2 routes: normal and blocking

app.get("/", (req, res) => {
res.status(200).send("normal non-blocking page");
});


app.get("/blocking", async (req, res) => {
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}


const result = fibonacci(45);


res.status(200).send(`fibonacci(45) is : ${result}`);
});


app.listen(port, () => {
console.log("Learning multithreading on port ", port);
});

So now let's start the server with

node index.js

And observe the response times in different scenarios:

Hitting “/” the first time:

response takes 12ms

Hitting “/blocking” first time:

Takes 14+ seconds

Hitting “/” while “/blocking” is running:

Takes 13.36 seconds (more than a 100,000% increase in response time)

Why this behavior?

When “/blocking” runs, it performs the Fibonacci calculation of 45. The recursive nature of the function makes it computationally expensive.

So when this runs, the event loop is blocked by this route, and even if you hit “/” at that time, Node can't serve the simple response of “/”, since it is single-threaded.

So how to fix this issue?

Enter Multithreading in NodeJs using the worker_thread module.

Worker Threads module

So in NodeJs V12.11, the worker_threads module was made stable, and they introduced it as

Workers (threads) are useful for performing CPU-intensive JavaScript operations.

This module enables the use of threads that execute JS in parallel.

Code executed in a worker thread runs in a separate child process preventing it from blocking your main operation.

Let's solve the above issue using the worker threads module.

Make a worker.js file

Here we will move the CPU-intensive function and execute the function.

Then pass the result using the postMessage function, which communicates with the main thread.

import { parentPort } from "worker_threads";

//move the function here
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const result = fibonacci(45);

// pass the result to parent thread
parentPort.postMessage(result)

Add the worker in the index2.js file

So now we make a copy of the index file in the index2 file, except we will make the new changes in the index2 file as follows:

import express from "express";

// import worker
import { Worker } from "worker_threads";

const app = express();
const port = process.env.PORT || 5000;

app.get("/", (req, res) => {
res.status(200).send("normal non-blocking page");
});

app.get("/blocking", async (req, res) => {

// constructor a worker, where you pass the path of your worker file
const worker = new Worker("./worker.js");

// handle when worker thread sends a message to parent
worker.on("message", (data) => {
res.status(200).send(`fibonacci(45) is : ${data}`);
});

// handle when worker thread sends an error to parent
worker.on("error", (error) => {
res.status(404).send(`failed to perform: ${error}`);
});
});

app.listen(port, () => {
console.log("Learning multithreading on port ", port);
});

Testing the API as per the 3 scenarios

So let's test the API based on the three scenarios above and observe how the response time has changed for 3rd scenario which is Send request to “/” while “/blocking” is in progress

Hitting “/” :

Takes 3ms

Hitting “/blocking”:

Takes 14 seconds as usual

Hitting “/” while “/blocking” is in progress:

takes 3ms (same as if it hit normally)

Why this improvement?

The “/blocking” is now running on the worker thread (child) while the “/” is working on the main thread. Both are independent of each other.

We avoid blocking the Event Loop, so it can serve other clients’ requests, improving our application performance.

Conclusion

Multithreading in NodeJS empowers Node to a great extent and nullifies the cons of it being a single-threaded run time. In this article, we also saw how we can solve an issue caused by single threading using the worker_threads module (with proof of API performance)

Worker Threads has a lot of other features for communication with the main thread, creating multiple child threads, etc. which can be read in the official worker threads documentation.

Note: Despite the multi-threaded superpowers added to single-threaded NodeJS, if you are building a scalable backend which has mostly CPU intensive tasks, it’s better to work with a language which supports multi-threading in itself like Java, Go Lang etc.

Thank you for reading, and happy coding!

Cheers 🥂

--

--

Subhanu
Engineering at Bajaj Health

🚀 22yr old software dev, building SaaS projects full time. I teach students, software development as a hobby and I am learning Spanish . Mucho gusto 💁‍♂️