Node.js
2026-01-22
4 min read

The Event Loop: What Actually Happens

A

Abhay Vachhani

Developer

Node.js is often described as "single-threaded," but if you look at your system monitor, you'll see it using multiple threads for I/O, crypto, and compression. The "single thread" refers specifically to the Event Loop—the orchestration mechanism that decides what code runs next. Understanding the loop is the difference between a Node.js developer and a Node.js engineer.

1. The Architecture: Stack, Heap, and Queues

To understand the loop, you must first understand three core components:

  • Call Stack: Where your synchronous code lives (LIFO). If the stack isn't empty, the Event Loop is paused.
  • Heap: Where objects and variables are allocated in memory.
  • Task Queues: Where asynchronous callbacks wait to be pushed onto the stack.

2. Microtasks vs. Macrotasks (Priority)

Not all asynchronous tasks are created equal. JavaScript distinguishes between Microtasks and Macrotasks (or just Tasks):

  • Microtasks: process.nextTick and Promise.then/catch. These have the highest priority and run immediately after the current operation finishes, before the loop continues to its next phase.
  • Macrotasks: setTimeout, setInterval, setImmediate, and I/O callbacks.

The Rule: The Event Loop will not move to the next phase until the Microtask Queue is completely empty.

3. Phases of the Event Loop

The loop moves through specific phases in a circle. In each phase, it executes a queue of callbacks:

  1. Timers: Executes setTimeout and setInterval callbacks.
  2. Pending Callbacks: Executes I/O callbacks deferred from the previous loop iteration.
  3. Idle, Prepare: Internal use only.
  4. Poll: Retrieves new I/O events; executes I/O related callbacks. If the Poll queue is empty, Node may wait here.
  5. Check: Executes setImmediate() callbacks.
  6. Close Callbacks: Executes close events, like socket.on('close', ...).

4. nextTick vs. setImmediate

Despite their names, process.nextTick fires "more immediately" than setImmediate.

  • process.nextTick() fires in the Microtask queue (between phases).
  • setImmediate() fires in the Check phase (after Poll).
console.log('Start');

setTimeout(() => console.log('Timeout'), 0);
setImmediate(() => console.log('Immediate'));
process.nextTick(() => console.log('NextTick'));

console.log('End');

// Output order: Start, End, NextTick, Timeout, Immediate

5. Don't Block the Event Loop!

Since the Event Loop is single-threaded, a heavy CPU-bound operation will "starve" the loop, preventing it from processing new requests. Common culprits include:

  • Complex Regular Expressions (Redos attacks).
  • Large JSON.parse() or JSON.stringify() calls.
  • Cryptographic functions used synchronously.
  • Large loops without asynchronous breaks.

Solution: Offload these tasks to Worker Threads or break them up using setImmediate() to allow the loop to breathe.

Conclusion

The Event Loop is the "heartbeat" of Node.js. By understanding how it schedules tasks, prioritizing Microtasks over Macrotasks, and avoiding blocking operations, you can build applications that handle massive concurrency with minimal latency. Remember: keep the stack clear so the loop can spin.

FAQs

Is Node.js truly single-threaded?

The Event Loop is single-threaded, but Node.js uses a thread pool (via Libuv) for heavy lifting like file system I/O and crypto.

Why does nextTick run before setImmediate?

`process.nextTick` is part of the microtask queue, which is processed immediately after the current operation, whereas `setImmediate` is a macrotask that runs in a specific phase of the loop.

How do I detect a blocked event loop?

You can use tools like `clinic.js` or monitor "event loop lag" by measuring the delay between a `setTimeout` execution and its requested time.