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.
According to Node.js documentation, a Node.js application runs using the event loop. The event loop is what allows Node.js to perform non-blocking I/O operations and explains how Node.js can be asynchronous. The event loop, aka the main thread, allows running one thing at a time. Having said that, Node.js JavaScript code runs on a single thread.
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.
What a Single-Threaded Process is
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
If these set of instructions are executed in a single-threaded process, the execution would look like the following:
What a Multi-Threaded Process is
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.
Why Node.js is Single-Threaded?
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:
- /getCars
- /updateCar
- /updateDriver
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:
- /getCars
- /updateCar
- /updateDriver
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.
Don’t Block the Event Loop (aka the Main Thread)
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”?
Yes.
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.
Confusing JavaScript and Node.js Threading Process
It is important to not say Node.js is single-threaded because the JavaScript programming language is single-threaded. This is incorrect. JavaScript can run in different programming environments, and Node.js being among the most popular environments using JavaScript. Therefore, it is a common misconception to think JavaScript is single-threaded. When speaking about single-threaded or multi-threaded, we should look at how the programming environment operates rather than how the language in itself.
What About Worker Threads in Node.js? Does it Make Node.js Multi-threaded?
While the implementation of worker threads in v10.5.0 allows the use of threads that execute JavaScript in parallel, Node.js event loop architecture is single-threaded based.
What really happens when spawning multiple threads using worker_threads is the generation of multiple V8 engines sharing memory. Workers threads are useful for performing CPU-intensive JavaScript operations. This frees up the main thread’s event loop from CPU-heavy processes and keeps it available for what is best for intensive I/O operations.
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.
Top comments (21)
NodeJS isn't more performant than Java. That's a common misconception. It's got better throughput since Javas old IO uses a thread per connection which doesn't scale (but is actually pretty fast). Async Java frameworks includes the benefits of threading and the throughput of NodeJS.
This will be resolved by project Loom which is fast approaching production. Project loom essentially turns threads "partially" green. That would mean you can code synchronously in Java using threads and the simpler stream IO. But the OS won't allocate a thread per Java thread. Only to the optimum amount of threads to get the most out of your CPU.
Thanks for setting the record straight. The author misspoke on true multithreading.
C# is even better it uses Tasks which always use the least busy CPU. It's not just another thread on a single process, it's an asynchronous call directly to the least busy CPU, which spins up, and queues up Task continuations. Only after the work is done are the results captured using an await. Similar to Javascript promise but many times speedier.
I have heard however that node too can spin up CPU agnostic instances. However this author omitted that discussion.
Node's async first architecture was a brilliant move and covers a lot of territory. However single node instances are nowhere the Speed of Java or C# using Tasks or other well defined threading techniques. The only problem is that browsers only speak Javascript.
Project loom aims to leapfrog C# by removing the need to denote tasks. It will make the JVM itself smart enough to use just the right amount of native threads seamlessly. It's currently in technology preview stage and hopefully will ship with Java 18 or 19.
Totally cool, I never liked the Task hoops to jump through but use it long enough and it's a second language.
Well I'm not an Java expert but every enterprise app using java like SAP products just feels slow and I instantly feel like I'm back in the 90's. Also he is saying that node is faster in this specific field which it is.
Statement means nothing really as we're not talking about any legacy product. We're talking about what can be done today. For true parallelism today, except on mainframes, the dotnet task construct is a true co-routine design which uses all CPUs in an agnostic way. There's nothing close to it today
If it is an server running an API, there are several programming languages with their respective frameworks that would work. C# could work using .NET Core or Entity Framework just like NodeJS with Express.js
Thanks for sharing! I didn't mean to say NodeJS is more performant than Java. The main reason I was providing the explanation was to give insights with regards to why it is widely used across several projects even though NodeJS architecture runs single-threaded as it provides the performance needed to develop APIs.
Hi @arealesramirez great article!
I would like to complement with the following:
Nice work! Keep it going :)
Please use the correct facts when referencing other environments like C# (and probably Java too on that topic). With dotnet (C#) you can choose to use AOT compilation when optimizing for startup time. But default it compiles to byte-code which during runtime is JIT compiled in multiple tiers, optimizing during the lifetime. It is even possible to create a profile from test runs to optimise before runtime.
Thanks for complementing.
As per my understanding, worker_threads will share memory but each will have each own independent event loop.
Kinda, the worker thread can share memory with other threads (made out of the same parent thread) or the parent thread itself. But they just create a new instance of the V8 engine, not another full Node process (where the Node process is who instantiate the event_loop). In Contrast, Cluster in fact instantiates one or more new Node process, for instance they come with it's own event_loop :)
Oh yeah! I see what you mean. Using pm2 is a good implementation of clusters where we could have a node per each core of the cpu.
Thanks for the clarifying about the event loop!
Exactly! 😄
No worries, happy to help! As I said, great work ;)
⚠️ I liked the way you try to clarify the matter, congratulations! I would say that having multiple instances of the V8 unable to communicate and not being fault tolerant only alleviates the problem. 😅
.NET can handle more than 7 million requests per second as of their .NET Core 2.2 back in 2019: ageofascent.com/2019/02/04/asp-net.... 8X faster than Node.js (at the time). .NET has seriously improved performance since then (as of .NET 6): https://www.techempower.com/benchmarks/#section=data-r20&hw=ph&test=plaintext&a=2 (now more than 10X faster than Node.js). Interpreted languages will likely never be able to complete with compiled languages as far as performance goes.
Javascript is Just in Time compiled via V8 engine but yes, still .NET is faster. C# gets JIT compiled via .NET runtime by the way.
The event loop link (nodejs.org/learn/the-nodejs-event-...) explanation does not exists. I think you can updated to this one: nodejs.org/en/learn/asynchronous-w....
Thanks for the great article!
Your descriptions of single thread Vs multi thread is nonsense. If you have a sequence of instructions a b c, then they must be executed in that order. You can't go and process c on another thread before b. Did you mean tasks?
Excellent article Andres.
Thanks @bendev12 !