Have you been reading multiple articles trying to understand whether Node.js is single-threaded or multi-threaded? Why are there many of them saying single-threaded and others saying multi-threaded? I’ve been there and after reading one article after another, it seems there’s always a doubt in the back of your mind telling you the concept is still not clear. In this article, I hope to clarify this confusion.
Now, there are several points you have probably read about in different articles such as using worker_threads making it multi-threaded, or the programming language used to develop Node.js applications makes it single-threaded, etc. I will cover those relevant points, but before we move forward, I’ll refresh your knowledge with regards to what single and multi-thread processes are.
A single-threaded process is the execution of programmed instructions in a single sequence. Having said that, if an application has the following set of instructions:
- Instruction A
- Instruction B
- Instruction C
A multi-threaded process is the execution of programmed instructions in multiple sequences. Therefore, instructions won’t have to wait to execute unless multiple instructions are grouped within different sequences.
Now you know Node.js architecture is single-threaded. However, why is it single-threaded? My first question for you is, do you understand how the event loop works? If not, I recommend you check this article.
However, to keep things simple, the event loop runs one process at a time. That means it can only execute one function at a time, and since functions can have multiple instructions, the event loop will execute one instruction at a time.
At first, it sounds not efficient providing poor performance. However, quite the opposite it turns out to be more performant and scalable than other multithreaded alternatives such as Java.
Running a multithreaded solution involves leveraging multiple cores of a system. Having said that, if one thread is waiting for an I/O response, the other threads could still be in progress. In theory, multithread seems the way to go, but what we are not taking into consideration is that a thread could still be blocked regardless of other threads being available.
The beauty of the event loop is not of running everything in a single thread, but it’s available to “put aside” long time-consuming I/O operations to keep the execution of other instructions. This is the reason why we get fast responses even though we could have multiple users making requests to a Node.js API at the same time.
The first thing to clarify is there is no such thing as making requests at the same time. It is perceived to have run requests at the same time, but in reality, the event loop runs processes defined for each request based on the order in which it arrived. Let’s make this concept simple to understand by using an example. In this case, we are going to assume we have the following API endpoints:
Remember, request are not made at the same time. The event loop will handle the requests in the following order assuming that was the order they were requested:
The event loop will execute the first instructions from the /getCars endpoint. At some point, there will be an instruction which is a request from the API to a database to fetch the cars. This is considered an I/O operation. This process can take a short or long time to execute. Regardless of how fast this gets executed. The event loop will trigger this request and move it “aside” to prevent blocking the thread from executing other instructions. However, it will resume triggering the set of instructions for the /getCars endpoint once a response is sent back from the database.
Therefore, while the request made from the /getCars endpoint to the database is triggered and waiting for a response, the /updateCar endpoint will trigger its set of instructions. If there is not I/O operation within the /updateCar endpoint, the /updateCar endpoint will return a response before the /getCars endpoint returns a response.
In a similar way, if the /updateCar endpoints have an instruction to execute an I/O operation, the event loop will trigger it but won’t block the thread from executing instructions. In this way, it could either start executing the set of instructions from the /updateDriver endpoint, or resume the execution of the /getCars endpoint once it receives a response from the database. This is based on whichever is added first in the event queue.
If you think about it, the major benefit of Node.js architecture is not the fact of being single-threaded, but its ability to not block the thread from executing other instructions. This is one of the main reasons Node.js is an excellent choice for developing APIs as these are heavily based on I/O operations. The event loop’s smart system to execute intensive I/O operations and resume processes once the I/O operations are completed while not worrying about issues that can come with using multithreaded solutions such as deadlocks or race conditions makes it a no brainer for many teams to use Node.js.
Like most solutions, there are advantages and disadvantages, and Node.js is not an exclusion of this. Since we know Node.js runs using the event loop, aka as the main thread, blocking the loop will indeed prevent the system from running other instructions regardless of whether they belong to a single process or multiple different processes.
Didn’t you say the event loop “triggers intensive operations and move them aside, resuming a process once the operations get a response”?
However, it is important to clarify the event loop’s ability to “resume” an I/O operation process doesn’t mean it will be capable of getting away around with an intensive CPU operation. The beauty of an I/O operation is to use external CPU processing power to execute a process. However, if our Node.js application is the one using intensive CPU processing power to execute power, it means we cannot execute other sets of instructions until the heavy processing power instruction completes. This is called blocking the event loop.
The expense of generating worker threads doesn’t result in a positive impact around I/O intensive work as in the end, each thread will have the same mechanism: one event loop per thread, which won’t be any different than opting not to use worker threads. Node.js’s built-in asynchronous I/O operations are more efficient than workers can be.
Having said that, each thread will use the same Node.js architecture which is single-threaded based. You can achieve multithreading by generating multiple nodes or Node.js V8 engines which in isolation are single-threaded. It is still correct to say Node.js is not multi-threaded.