Is Node.js single-threaded?
Is Node.js single-threaded? What happens when I run “node myscript.js”?
No, Node.js is not single-threaded, it has never been. This is a very common misunderstanding among the dev community.
Javascript indeed is single-threaded and the Node.js event loop runs into a single thread. But that’s not all about the runtime.
A key component of the runtime, responsible for managing the event loop and async operations, is libuv. Libuv itself has a thread pool, composed by default of 4 threads (you can change this number by setting the process.env.UV_THREADPOOL_SIZE value), that is started every time you run the node command.
For some functions from the std lib, libuv decides to run computationally intensive tasks outside the event loop and uses its thread pool to do so.
Some tasks don’t even follow the thread pool avenue and are delegated to the underlying OS, like the network requests using the https module, for example.
How Node.js manages the event loop, its thread pool and OS async helpers is what makes this runtime so powerful. 🚀
To answer the second question, keep in mind that:
First of all, the javascript synchronous code is executed into the thread’s call stack, one step after another.
All the callbacks (for example setTimeout(callback1, 0), process.nextTick(callback2), Promise.then(callback3), etc) are sent to the libuv event loop queues.
The event loop will start picking these callbacks from its queues and executing them into the call stack after the js sync execution finishes.
The event loop steps/queues are the following, in order of priority and execution:
Microtask Queues
Composed of two queues: the process.nextTick() callbacks queue and the Promise callbacks queue. The Microtask Queues have the highest priority when it comes to executing asynchronous code in Node.js.
Timers Queue
Holds callbacks associated with setTimeout and setInterval. The callbacks are executed in the first-in-first-out order (FIFO). When there are multiple setTimeout executed with different delays, the event loop queues up the one with the shortest delay first and executes it before the others.
I/O Queue
To add callbacks to the I/O Queue we can use most of the async methods from the std lib, for example fs.readFile().
I/O Pooling
In the previous example, libuv will delegate the readFile operation to its thread pool and the callback will be queued to the I/O Queue by the I/O Pooling step only after the I/O operation completed.
Check Queue
Contains the callbacks associated with the setImmediate() function.
Close Queue
Holds the callbacks of close events, for example, readableStream.on(‘close’, callback).
The event loop continues running until there is no pending task being executed by the thread pool and the OS sync helpers, and all the queues are empty.
The Microtask queues are executed in between each queue and in between each callback of the Timer and Check queues.