Skip to main content

Command Palette

Search for a command to run...

Node.js Internals Explained in Simple Words

Published
β€’9 min read
Node.js Internals Explained in Simple Words

🧠 What is Node.js?

Node.js is a JavaScript runtime environment that allows JavaScript to run outside the browser.

But internally, Node.js is not just JavaScript.

It is built using:

V8 Engine + libuv + C++ bindings

  • V8 Engine β†’ Executes JavaScript code (turns JS into machine code)

  • libuv β†’ Handles asynchronous operations (event loop + thread pool)

  • C++ bindings β†’ Bridge between JavaScript and system (OS)

πŸ—οΈ Core Architecture of Node.js

1. Process (What happens when Node starts)

When you run a Node.js file, a process is created.

A process means:

A running instance of a program with memory, resources, and execution flow.

2. Main Thread (Very Important Concept)

Node.js runs on a single main thread.

This main thread is responsible for:

  • executing JavaScript code

  • handling callbacks

  • managing the event loop

  • delegating heavy tasks

πŸ‘‰ BUT it does NOT handle heavy work directly.

Instead, it delegates them to libuv thread pool or OS system APIs.

πŸ”„ What happens inside the Main Thread?

When Node starts execution, it follows these steps:

A. Project Initialization Phase

Node loads your project:

  • reads entry file (index.js / app.js)

  • initializes memory

  • prepares runtime environment

B. Top-Level Code Execution

All synchronous code runs first:

  • variable declarations

  • console logs

  • function definitions

πŸ‘‰ This is the first execution phase

C. Import / Module Loading Phase

When you write:

import fs from "fs";

Node does this:

What happens internally:

  • Resolves module path

  • Loads built-in modules (fs, crypto, path, etc.)

  • Wraps your file inside a function scope

  • Executes module code BEFORE your main logic continues

πŸ‘‰ That’s why imports are NOT random β€” they are resolved first.

D. Callback Registration Phase

At this stage:

Node sees async functions like:

  • setTimeout

  • setImmediate

  • fs.readFile

  • Promises

πŸ‘‰ It does NOT execute them immediately.

Instead, it:

Registers them in the background system (event loop queues / thread pool / OS handlers)

E. Event Loop Starts

After top-level code is done:

πŸ‘‰ libuv starts the event loop

Now Node continuously checks:

  • timers

  • I/O operations

  • callbacks

  • thread pool results

πŸ” Event Loop Phases

1. Timers phase(expired callbacks):

This phase runs:

  • setTimeout

  • setInterval

Why called β€œexpired callbacks”?

Because:

Only timers whose delay has finished are executed here.

If delay = 0 β†’ it becomes eligible immediately, but still depends on event loop timing.

2. I/O Polling Phase:

I/O polling is the phase of the event loop where Node.js interacts with the background system to handle asynchronous operations. When tasks like file reading, crypto hashing, or DNS lookups are triggered, Node.js first delegates them to the libuv thread pool or OS, so the main thread doesn’t get blocked. During the polling phase, the event loop continuously checks whether these background tasks are completed, collects their results once they finish, and prepares their callbacks to be executed in the next appropriate phase of the event loop.

πŸ‘‰ Event loop does NOT do heavy work itself.

Instead:

  • libuv thread pool does the work

  • event loop just waits + checks results

That waiting + checking is called:

I/O Polling

3. setImmediate() Phase :

The setImmediate() phase is the part of the event loop where Node.js executes all callbacks scheduled using setImmediate(). After the I/O polling phase finishes collecting results from background tasks, the event loop moves into this stage and runs these callbacks right away in the same iteration. This phase ensures that tasks scheduled with setImmediate() are executed immediately after I/O operations are completed, but before the event loop proceeds to handle close callbacks or begins the next loop cycle.

setImmediate() is a Node.js function used to schedule a callback to run after the I/O polling phase of the event loop completes. It does not depend on time like setTimeout; instead, it depends on the event loop cycle. When setImmediate() is called, its callback is placed into the check queue, and it is executed in the same event loop iteration right after I/O tasks are finished.

4. Close callback() phase :

The Close Callback phase is the part of the event loop where Node.js executes callbacks that are triggered when certain resources are closed or cleaned up. This includes events like closing a socket, ending a file stream, or shutting down a connection. After all other phases like timers, I/O polling, and setImmediate() are completed, Node.js enters this phase to safely handle cleanup-related tasks so that resources are properly released before the event loop moves to the next iteration or exits if nothing is left to process.

example :

socket.on("close", callback)

or

process.on("SIGINT", () => {
  console.log("Ctrl + C pressed (SIGINT received)");
  console.log("Cleaning up before exit...");
  process.exit();
});

console.log("Running... Press Ctrl + C");

5. Check for pending tasks :

After each cycle:
Node checks:

  • Is anything pending?

  • Any timer left?

  • Any I/O left?

πŸ‘‰ If YES β†’ next loop iteration
πŸ‘‰ If NO β†’ process exits

After each event loop cycle, Node.js checks whether there is any pending work such as active timers, ongoing I/O operations, or open handles like servers and streams. If any of these exist, the event loop continues to the next iteration. If nothing is left, Node.js safely exits the process. setImmediate() is not directly part of this exit check; it is simply executed during the event loop cycle when reached.

πŸ§ͺ Example 1 (Basic Event Loop Understanding)

import fs from "fs";

setTimeout(() => {
  console.log("inside setTimeout");
}, 0);

setImmediate(() => {
  console.log("inside setImmediate 1");
});

fs.readFile("sample.txt", "utf-8", function (err, data) {
  console.log("file read success");

  setTimeout(() => {
    console.log("inside setTimeout 2");
  }, 0);

  setTimeout(() => {
    console.log("inside setTimeout 3");
  }, 0);

  setImmediate(() => {
    console.log("inside setImmediate 2");
  });
});

console.log("top level code");

🧠 Execution Explanation (Step by Step)

Step 1: Top-level code runs first

Step 2: Timers & Immediate are registered

They go into event loop queues:

  • setTimeout β†’ timer queue

  • setImmediate β†’ check queue

Step 3: File reading starts

  • libuv thread pool handles it

  • main thread continues

Step 4: Event loop begins

Order depends on phases:

Typical output:

top level code
inside setTimeout
inside setImmediate 1
file read success
inside setImmediate 2
inside setTimeout 2
inside setTimeout 3

πŸ‘‰ BUT exact order can vary slightly depending on system timing.

⚑ Example 2 (CPU Intensive + Thread Pool Deep Understanding)

import fs from "fs";
import crypto from "crypto";

const start = Date.now();

// Thread pool size
process.env.UV_THREADPOOL_SIZE = 4;

setTimeout(() => {
  console.log("Hello from Timer");
}, 0);

setImmediate(() => {
  console.log("Hello from Immediate");
});

fs.readFile("sample.txt", "utf-8", function (err, data) {
  console.log("File Reading Complete");

  setTimeout(() => console.log("Time 2"), 0);
  setTimeout(() => console.log("Time 3"), 0);

  setImmediate(() => console.log("Immediate 2"));

  // CPU intensive tasks (thread pool)
  crypto.pbkdf2("password", "salt", 300000, 1024, "sha256", () => {
    console.log("Password 1 hashed", Date.now() - start);
  });

  crypto.pbkdf2("password", "salt", 300000, 1024, "sha256", () => {
    console.log("Password 2 hashed", Date.now() - start);
  });

  crypto.pbkdf2("password", "salt", 300000, 1024, "sha256", () => {
    console.log("Password 3 hashed", Date.now() - start);
  });

  crypto.pbkdf2("password", "salt", 300000, 1024, "sha256", () => {
    console.log("Password 4 hashed", Date.now() - start);
  });

  crypto.pbkdf2("password", "salt", 300000, 1024, "sha256", () => {
    console.log("Password 5 hashed", Date.now() - start);
  });
});

console.log("Hello from Top Level Code");

🧠 What happens internally?

  1. First 4 crypto tasks run in thread pool

Because default pool size = 4

  1. 5th crypto task waits

Because no thread is free

  1. As soon as one finishes

5th task starts

⚑ Key Insight

πŸ‘‰ Thread pool is like a small worker team πŸ‘‰ If workers are busy β†’ tasks wait in queue

πŸ“¦ Queues in Node.js Event Loop

Node.js event loop has several important queues where different types of callbacks are stored before execution:

1. ⏱️ Timer Queue:

  • Stores callbacks from setTimeout() and setInterval()

  • Runs when the timer duration has expired

2. πŸ“‘ I/O Queue (Poll Queue)

  • Stores callbacks from I/O operations like: file reading (fs) network requests database operations

  • These are executed after background tasks are completed

3.⚑ Check Queue

  • Stores callbacks from setImmediate()

  • Runs immediately after the I/O polling phase

4. πŸ”š Close Queue

  • Stores callbacks for cleanup events

  • Example: socket close events stream close events

5. πŸ” Microtask Queue (Higher Priority)

  • Runs between phases:

- process.nextTick() (highest priority)

- Promises (.then, .catch)

πŸ‘‰ Microtasks run before moving to the next queue

Note: Different internal systems (like libuv’s timer system, thread pool, and OS) are responsible for pushing callbacks into their respective queues when they become ready, while the event loop’s job is simply to process and execute those callbacks phase by phase.

🧠 Who puts expired callbacks into queues?

Node.js internal systems (libuv + timers system + OS) put callbacks into queues β€” NOT the event loop itself.

1. Timers (setTimeout, setInterval)

When you write:

setTimeout(fn, 1000);

What happens:

  • Node registers the timer and starts counting time

  • Internally, libuv maintains a timer system (like a timer list/heap)

πŸ‘‰ When time expires:

libuv moves the callback into the timer queue

βœ” Event loop did NOT do this
βœ” libuv/timer system did it

2. I/O operations (fs, network, crypto)

When you write:

fs.readFile("file.txt", fn);

What happens:

  • Task goes to thread pool or OS

  • Work happens in background

πŸ‘‰ When task finishes:

OS / libuv pushes the callback into the I/O queue

3. setImmediate()

setImmediate(fn);

πŸ‘‰ This is simpler:

  • Callback is directly placed into the check queue during registration

  • libuv stores the callback in the check queue

(No waiting, no background work)

4. Close callbacks

socket.on("close", fn);

πŸ‘‰ When resource closes:

Node pushes callback into close queue