I was reading the Node.js official documentation to understand how Node.js actually works internally.
But honestly some parts felt a bit hard to me
So i thought why not to rewrite what i learn in simple words that will help you as well. So in this blog we will cover the following topics.
Topics to Cover
- What is Node.js
- V8 Engine and libuv
- Node.js Architecture Overview
- How Node.js Executes a Program (node index.js)
- Thread Pool in Node.js
- Event Loop and Its Phases
- Understanding Event Loop with Examples
- Worker Thread
- Conclusion
Don't worry this blog will cover each and every topic from the Node.js official documentation.
What is Node.js
Originally JavaScript is meant to be run on the browser like Google chrome but in 2009 a developer name Ryan Dahl had a different idea.
He extract the Google V8 engine from the browser an combined it with a powerful library called libuv. Using C++ and JavaScript he build a runtime that could execute JavaScript outside the browser.
Later Node.js was created which provide a runtime environment that lets you run JavaScript outside the browser means you can run it on sever (backend).
Without Node.js:
JavaScript → Browser
With Node.js:
JavaScript → Server
Node.js is made up of 2 major component which provide this runtime environment
V8 Engine + libuv + C++ = Node.js
V8 Engine :- V8 engine is a Google Chrome engine that convert JavaScript code into machine language so the computer can execute it quickly
Libuv :- It is a library that helps Node.js handle asynchronous tasks.
It manages things like:
- file reading
- network requests
- timers
- event loop
- thread pool
Done worry we'll explain this in further later in this topic.
Node.js architecture diagram
Before we understand how Node.js runs our code, let's first look at the high-level architecture diagram.
Node.js Execution
Let’s suppose we have a JavaScript file named index.js.
If we want to run this file using Node.js we simply open the terminal and run the following command:
When we run the command node index.js, Node.js creates a process.
This process runs on a single thread also called the main thread, which is responsible for executing our JavaScript code. Without this JavaScript did not run.
The main thread first initializes the Node.js environment for the program. After that it starts executing the top-level code inside the index.js file
Top level code :- It simply means the code that runs immediately when
the file is executed, not inside any function or callback.
example :- console.log("hello world")
const fs = require("fs");
While executing that top-level code, Node.js also performs a few important tasks such as :
- It loads any modules using require()
- Registers event callbacks (such as timers or I/O callbacks)
- Prepares internal resources needed to run the application
Node.js also creates a thread pool (managed by libuv). This thread pool is used to handle CPU-intensive or blocking tasks, such as:
- cryptography
- hashing
- file system operations
- DNS lookups
Even though JavaScript is single-threaded, Node.js uses a thread pool to handle heavy tasks in the background. By default, this thread pool has 4 threads, but you can increase or decrease the number of threads depending on how heavy your workload is.
Finally, the main thread starts the event loop, which continuously checks for tasks and handles asynchronous operations by offloading heavy work to the thread pool when necessary.
Now lets see the internals of Event loop and its phases
Event Loop Starts
After the top-level code finishes executing, the main thread starts the Event Loop.
The event loop is responsible for continuously checking if there are any tasks waiting to be executed. It allows Node.js to handle asynchronous operations without blocking the main thread.
If a task requires heavy work (like file reading, cryptography, or DNS), the event loop offloads that task to the thread pool managed by libuv.
Once the task is finished, its callback is placed back in the queue so it can be executed by the main thread.
Event Loop Phases
Once the event loop starts, it keeps running in a cycle and checks different phases to see if there are any tasks to execute.
These phases are as follows :
Expired Timer Callbacks
Runs the callbacks ofsetTimeout()andsetInterval()when their time is finished.I/O Polling
Checks if any I/O task (like file reading or network request) has finished and runs its callback.setImmediate Callbacks
Runs callbacks scheduled usingsetImmediate().Close Callbacks
Runs callbacks when something is closed, like a socket or connection.
If there are still pending tasks, the event loop continues running.
If there are no timers, no callbacks, and no pending I/O, Node.js exits the event loop and the program finishes execution.
Now that we understand the phases of the event loop, let's look at a simple example to see how these phases work in practice.
Examples
Before looking at the example, first understand the basic flow.
- Top-level code executes first
- Event Loop starts
- Timers phase → runs expired setTimeout / setInterval callbacks
- I/O polling phase → checks completed I/O operations
- setImmediate phase → runs setImmediate() callbacks
- Close callbacks phase → runs close-related callbacks
- If tasks are still pending → loop runs again
- If no tasks remain → Node.js exits
Example 1
console.log("Start");
setTimeout(() => { console.log("Timer callback"); }, 0);
setImmediate(() => { console.log("Immediate callback"); });
console.log("End");
Output of above code
Start
End
Timer callback
Immediate callback
Whay this happens
-
console.log("Start")runs first because it is top-level code. -
setTimeout()registers a timer callback. -
setImmediate()registers an immediate callback. -
console.log("End")runs because it is also top-level code. - Now the Event Loop starts.
- The event loop first checks timers → runs
"Timer callback". - Then it goes to setImmediate phase → runs
"Immediate callback".
## Note
You may sometimes see setImmediate() run before setTimeout().
This happens because both callbacks are scheduled for different phases
of the event loop. Depending on how quickly the event loop starts
the immediate callback can run first..
So the output can be:
Start
End
Immediate callback
Timer callback
Example 2
const fs = require('node:fs');
console.log("Start");
setTimeout(() => {
console.log("Timer callback");
}, 0);
setImmediate(() => {
console.log("Immediate callback");
});
fs.readFile("sample.txt", () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
console.log("End");
Output of above code
Start
End
Timer callback
Immediate callback
immediate
timeout
Why this happens
-
console.log("Start")runs first because it is top-level code. -
setTimeout()registers a timer callback. -
setImmediate()registers an immediate callback. -
fs.readFile()starts an I/O operation in the background. -
console.log("End")runs because it is also top-level code.
So far we have seen how the event loop handles different callbacks, but what happens if there is a CPU-intensive task?
What if there is a CPU-intensive task?
In Node.js, heavy tasks do not run inside the event loop because that would block the main thread.
Instead, these tasks are sent to the thread pool (managed by libuv). The thread pool uses worker threads to perform the heavy work in the background.
Lets take some CPU intensive works and see the behaviour of threads and how we can increase the size of threadpool.
Example 1
const crypto = require("node:crypto");
const fs = require("node:fs");
const start = Date.now();
process.env.UV_THREADPOOL_SIZE = 4
fs.readFile("sample.txt", "utf-8", () => {
console.log("I/O Polling Finished");
crypto.pbkdf2("password1", "salt1", 100000, 1024, "sha512", () => {
console.log(`${Date.now() - start}ms`, "Password 1 Done");
});
crypto.pbkdf2("password2", "salt1", 100000, 1024, "sha512", () => {
console.log(`${Date.now() - start}ms`, "Password 2 Done");
});
crypto.pbkdf2("password3", "salt1", 100000, 1024, "sha512", () => {
console.log(`${Date.now() - start}ms`, "Password 3 Done");
});
crypto.pbkdf2("password4", "salt1", 100000, 1024, "sha512", () => {
console.log(`${Date.now() - start}ms`, "Password 4 Done");
});
});
Output of the above code
I/O Polling Finished
1115ms Password 3 Done
1132ms Password 1 Done
1142ms Password 2 Done
1157ms Password 4 Done
From the above code, we saw that the hashing tasks finished almost at the same time. This happens because the thread pool has 4 threads, so it can run 4 tasks at the same time.
I/O Polling Finished
1030ms Password 3 Done
1038ms Password 1 Done
1071ms Password 4 Done
1082ms Password 2 Done
2005ms Password 5 Done
2009ms Password 6 Done
But when we increase the number of tasks while the number of threads is still 4, some tasks have to wait.
The 5th and 6th tasks cannot start right away because all threads are busy.
So they wait until a thread becomes free, and that’s why they finish later.
Note: By default Node.js uses 4 threads in the thread pool, but you can increase or decrease it using
UV_THREADPOOL_SIZE, depending on your CPU cores and workload.
Conclusion
In this blog, we understood how Node.js works internally. We started with what Node.js is, then looked at the V8 engine and libuv, and saw how Node.js executes a program when we run node index.js.
We also learned how the event loop works, its phases, and how Node.js handles asynchronous operations without blocking the main thread.
Finally, we saw how CPU-intensive tasks are handled using the thread pool, allowing Node.js to stay fast and responsive.
Understanding these concepts gives you a clear mental model of how Node.js works behind the scenes.
Thanks for reading ! if enjoyed this blog , you can read more on this 👇


Top comments (0)