Skip to main content
Back to blog
March 13, 20265 min read

Mastering the Event Loop: The Heart of Node.js

If you work with Node.js, you’ve probably heard it’s "single-threaded.
Nodejseventloop
Mastering the Event Loop: The Heart of Node.js

how can it handle thousands of concurrent connections with just one thread? The answer lies in the Event Loop.

What is the Event Loop?

It is the mechanism that allows Node.js to perform non-blocking I/O operations. Think of a waiter in a restaurant: they don’t stand by the table waiting for the chef to cook the meal. They take the order, bring it to the kitchen, and move to the next table. Once the food is ready, they return to serve it.

The Main Phases

The loop isn't chaotic; it follows a strict sequence:

  1. Timers: Executes setTimeout and setInterval callbacks that have expired.

  2. Poll: This is where Node looks for new I/O events (file reading, network). This is where the loop spends most of its time.

  3. Check: Specifically reserved for setImmediate callbacks.

Microtasks vs Macrotasks: The Invisible Hierarchy

This is a common pitfall in technical interviews.

  • Macrotasks: These are standard loop events (timers, I/O).

  • Microtasks: These include Promises and process.nextTick().

The Golden Rule: Microtasks are executed immediately after the current phase of the Event Loop finishes, before moving on to the next macrotask. If you flood your code with recursive microtasks, you will "starve" the Event Loop, and it will never reach the I/O phase.

In what order does my code execute?

Title: Microtasks, Macrotasks, and Execution Order in Node.js

Have you ever wondered why a Promise executes before a setTimeout(0)? The answer lies in queue priority. Let’s look at it with a real-world example you might encounter in a technical interview.

The Code Experiment

Take a look at this block and try to guess the console output order:

JavaScript

console.log('1. Start (Synchronous)');

setTimeout(() => console.log('2. Timeout (Macrotask)'), 0);

setImmediate(() => console.log('3. Immediate (Check phase)'));

Promise.resolve().then(() => console.log('4. Promise (Microtask)'));

process.nextTick(() => console.log('5. NextTick (Top Priority)'));

console.log('6. End (Synchronous)');

The Result Explained

  1. Synchronous First: 1 and 6 are printed. Synchronous code blocks the loop until it's done.

  2. Draining Microtasks: Before the loop moves to the Timers phase, it clears the microtask queues:

    • process.nextTick wins: Prints 5.

    • Promises are next: Prints 4.

  3. Event Loop Kicks In:

    • Timers Phase: It sees the 0ms setTimeout has expired. Prints 2.

    • Check Phase: setImmediate is executed. Prints 3.

Final Order: 1, 6, 5, 4, 2, 3.

Why does this matter? If you overuse process.nextTick, you can cause "I/O Starvation," preventing the loop from ever reaching the file system or network phases.

Comments