Skip to main content

Command Palette

Search for a command to run...

🚀 Beyond the Surface: Understanding the Node.js Event Loop

Updated
6 min read
🚀 Beyond the Surface: Understanding the Node.js Event Loop
S
Software Engineer specializing in backend development and API architecture with 6+ years of experience. I build scalable web applications using PHP, REST/GraphQL APIs, and modern JavaScript, with strong expertise in WordPress, headless CMS solutions, and system integrations. Passionate about creating secure, high-performance systems that translate complex business needs into efficient technical solutions.

Most developers use Node.js every day because it is fast and non-blocking.
But if someone asks:

How does Node.js handle thousands of connections with a single thread?

Many developers struggle to explain it clearly.

To truly understand Node.js, we need to go beyond the surface and explore the engine that makes it all possible: the Event Loop.


1️⃣ JavaScript Is Just the Language

One of the biggest “Aha!” moments for many developers is realizing that JavaScript itself is very small.

JavaScript only provides:

  • variables

  • functions

  • objects

  • syntax

  • logic

Things like:

  • setTimeout

  • fetch

  • console.log

  • file operations

  • networking

are NOT part of JavaScript.

They come from the runtime environment.

Two common runtimes are:

Runtime APIs Provided
Browser DOM APIs, fetch, timers
Node.js File system, networking, timers

Node.js allows JavaScript to run outside the browser, especially on servers.

To do this efficiently, Node.js relies on a powerful library written in C++ called libuv.


2️⃣ The Role of libuv

libuv is the engine behind Node.js asynchronous behavior.

It provides:

  • Event Loop implementation

  • Non-blocking I/O

  • Thread Pool

  • Networking operations

  • File system operations

Because of libuv, Node.js can handle operations like:

  • reading files

  • database calls

  • network requests

  • timers

without blocking the main thread.

This is what makes Node.js scalable and efficient.


3️⃣ What Happens Before the Event Loop Starts?

Before the Event Loop even begins running, Node.js follows a specific startup process.

Step 1 — Initialize Environment

Node prepares the runtime environment.

Step 2 — Execute Top-Level Code

Your JavaScript file runs line by line.

Step 3 — Register Async Tasks

Async operations are registered with libuv.

Example:

setTimeout(() => console.log("Timer"), 0);

This does not execute immediately.

Instead it gets registered.

Step 4 — Start the Event Loop

Once all synchronous code is executed, the Event Loop starts processing tasks.


4️⃣ Event Loop Architecture

At a high level, Node.js works like this:

JavaScript Code
      ↓
     V8 Engine
      ↓
     Node.js APIs
      ↓
      libuv
      ↓
Event Loop + Thread Pool

The Event Loop continuously checks queues and executes callbacks.


5️⃣ The 5 Phases of the Event Loop

The Event Loop is not random.

It processes tasks in a strict order of phases.

Phase Purpose Example
Timers Execute expired timers setTimeout, setInterval
I/O Callbacks Process system callbacks TCP errors
Poll Retrieve new I/O events File read, network
Check Execute setImmediate callbacks setImmediate()
Close Cleanup callbacks socket.close()

Think of the event loop like a security guard walking through rooms in a building.

Each room is a phase, and the guard must check them in the same order every time.


6️⃣ Example: Understanding Execution Order

Consider this code:

import fs from "fs";

console.log("Start");

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

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

fs.readFile("test.txt", () => {
  console.log("File Read Complete");
});

console.log("End");

Possible output:

Start
End
Timer or Immediate
File Read Complete

Why?

Because:

1️⃣ Synchronous code runs first

2️⃣ Async tasks are registered

3️⃣ Event loop begins processing phases


7️⃣ The Timer vs Immediate Mystery

This is where many developers get confused.

Example:

setTimeout(() => console.log("Timer"), 0);
setImmediate(() => console.log("Immediate"));

At top level, the order is not guaranteed.

Sometimes you will see:

Timer
Immediate

Sometimes:

Immediate
Timer

This happens because the event loop scheduling depends on process timing.


8️⃣ Inside an I/O Callback

Now consider this:

import fs from "fs";

fs.readFile("test.txt", () => {

  setTimeout(() => console.log("Timer"), 0);

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

});

Output will always be:

Immediate
Timer

Why?

Because after the Poll phase (file read) the event loop moves directly to the Check phase.

And setImmediate runs in the Check phase.

Timers will execute in the next loop cycle.


9️⃣ Why Does setImmediate Exist?

If we already have setTimeout(fn, 0), why was setImmediate introduced?

Two major reasons:

1️⃣ Run Code After I/O

setImmediate guarantees execution right after I/O completes.

Example:

fs.readFile("data.txt", () => {

  setImmediate(() => {
    console.log("Process file immediately after read");
  });

});

2️⃣ Prevent Event Loop Blocking

Large computations can block the event loop.

Example solution:

function processLargeData(data) {

  let chunk = data.splice(0, 1000);

  process(chunk);

  if (data.length > 0) {
    setImmediate(() => processLargeData(data));
  }

}

This allows Node.js to process other incoming requests between chunks.


🔟 Thread Pool: The Hidden Workers

Even though Node.js runs JavaScript on a single thread, libuv includes a thread pool.

It handles heavy tasks like:

  • file system operations

  • DNS lookups

  • compression

  • cryptography

Default thread pool size:

4 threads

This allows Node.js to handle multiple blocking operations simultaneously.


11️⃣ Real-World Example

Imagine a server receiving thousands of requests.

Instead of blocking the thread:

Request arrives
↓
Node registers callback
↓
libuv performs I/O
↓
Event Loop executes callback

This design allows Node.js to scale efficiently.


📌 Key Takeaways

• JavaScript itself does not provide async APIs

• The runtime environment provides them

• Node.js relies on libuv for async operations

• The Event Loop runs tasks in phases

• Understanding phases helps write better asynchronous code


🎯 Final Thoughts

Understanding the Node.js Event Loop completely changed how I write asynchronous code.

Instead of guessing when callbacks will run, I now understand how the system schedules them.

Node.js isn’t magic.

It’s simply a well-designed system of queues, phases, and callbacks working together.

And once you understand it, the entire Node.js ecosystem suddenly becomes much clearer.