Node.js: Async: Event Loop

Last updated: 2026-08-26

1. Story: A Service Blocked by 200 ms

Bob’s API service typically responds in just 5 ms, but during peak hours, response times skyrocketed to 2,000 ms. Users kept complaining, and at first, he thought the database was slow, but adding indexes didn’t help. Later, using console.time to troubleshoot step by step, he discovered that sorting a large synchronous JSON array was taking 200 ms—this 200-ms operation was blocking the event loop, causing all queued requests to wait for it to finish. After understanding how the event loop works, Bob used setImmediate to break the sorting into smaller chunks and execute them in turn, reducing response times during peak hours to 50 ms.

You'll learn:



2. The 6 Stages of the Event Loop

The Node.js event loop is driven by libuv and proceeds through six phases in sequence. Each loop cycle begins with "timers" and ends with "close callbacks," then returns to "timers" to start the next cycle.

100%
flowchart TD
    A["timers<br/>setTimeout / setInterval"] --> B["pending callbacks<br/>System-Level Callbacks"]
    B --> C["idle, prepare<br/>For internal use only"]
    C --> D["poll<br/>I/O callbacks / new I/O polling"]
    D --> E["check<br/>setImmediate"]
    E --> F["close callbacks<br/>socket.on('close')"]
    F --> A
Phase Callback Type Description
timers setTimeout / setInterval Executes the timer callback when it expires, subject to a minimum delay of 1 ms
pending callbacks system-level callbacks I/O callbacks deferred to the next loop (e.g., TCP errors)
idle, prepare Internal use Internal use by libuv; developers generally do not interact with it
poll I/O callback Checks for new I/O events and executes I/O-related callbacks; if there are no timers, the process will block at this stage
check setImmediate The setImmediate callback is executed at this stage
close callbacks close events Such as socket.on('close') close callbacks


3. Microtasks and Macrotasks

(1) (3.1) Microtask Queue

Microtasks are executed in their entirety after the current phase ends and before the next phase begins. The microtask queue has two priority levels:

Micro-task Priority Description
process.nextTick Maximum Prioritize clearing the nextTick queue after each phase ends
Promise.then / catch / finally Second highest Executed after the nextTick queue is cleared

▶ Example: nextTick takes precedence over Promise.then

JAVASCRIPT
process.nextTick(() => {
  console.log('nextTick 1');
});

Promise.resolve().then(() => {
  console.log('promise.then 1');
});

process.nextTick(() => {
  console.log('nextTick 2');
});

Promise.resolve().then(() => {
  console.log('promise.then 2');
});
▶ Try it Yourself
TEXT 📖 Display only
nextTick 1
nextTick 2
promise.then 1
promise.then 2

(2) (3.2) Macrotask Queue

Macrotasks are scheduled by each phase of the event loop, with each phase executing its corresponding macrotasks.

Macro Task Phase Description
setTimeout / setInterval timers Minimum delay of 1 ms; enters the timers queue upon expiration
setImmediate check Executed during the "check" phase of the next iteration
I/O Callback poll Callback upon completion of I/O operations such as file reads/writes and network operations
close callbacks close callbacks Close event callbacks


4. setTimeout vs setImmediate vs process.nextTick

▶ Example: The execution order of outer contexts is undefined

JAVASCRIPT
setTimeout(() => {
  console.log('setTimeout');
}, 0);

setImmediate(() => {
  console.log('setImmediate');
});
▶ Try it Yourself

In a non-I/O context, the execution order of setTimeout and setImmediate is undefined—it depends on the system's process scheduling, and either one may execute first.

▶ Example: In an I/O context, setImmediate is always executed first

JAVASCRIPT
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);

  setImmediate(() => {
    console.log('setImmediate');
  });
});
▶ Try it Yourself
TEXT 📖 Display only
setImmediate
setTimeout

I/O callbacks are executed during the poll phase; once they are complete, the process moves to the check phase (setImmediate), and only then proceeds to the next round of the timers phase (setTimeout).

▶ Example: process.nextTick always comes first

JAVASCRIPT
setTimeout(() => {
  console.log('setTimeout');
}, 0);

setImmediate(() => {
  console.log('setImmediate');
});

process.nextTick(() => {
  console.log('nextTick');
});
▶ Try it Yourself
TEXT 📖 Display only
nextTick
setTimeout   (or setImmediate, depends on scheduling)
setImmediate
Feature setTimeout(fn, 0) setImmediate process.nextTick
Queue Macro Tasks (timers) Macro Tasks (check) Microtasks (highest priority)
Execution Phase timers check Immediately after the current phase ends
Minimum Latency 1 ms None None
I/O Context Order After setImmediate Before setTimeout First
Outer Context Order Undefined Undefined Earliest


5. Comparison of Microtasks vs. Macrotasks

Dimension Micro-task Macro-task
Representative process.nextTick, Promise.then setTimeout, setImmediate, I/O callbacks
Execution Timing After the current phase ends and before the next phase begins During the corresponding phase of the event loop
Execution Strategy Clear All at Once Execute One Per Phase, Then Check the Microtasks
Nested Microtasks generated during execution are also cleared in this round Pushed to a later stage for execution
Priority Higher than all macro tasks Lower than microtasks

▶ Example: Micro-tasks interspersed among macro-tasks

JAVASCRIPT
setTimeout(() => {
  console.log('timeout 1');
  Promise.resolve().then(() => {
    console.log('promise between 1 and 2');
  });
}, 0);

setTimeout(() => {
  console.log('timeout 2');
}, 0);
▶ Try it Yourself
TEXT 📖 Display only
timeout 1
promise between 1 and 2
timeout 2

After each macro-task is completed, the micro-task queue is cleared before the next macro-task is executed.



6. The Dangers of Blocking the Event Loop and How to Avoid Them

▶ Example: Blocking the Event Loop

JAVASCRIPT
const http = require('http');

const server = http.createServer((req, res) => {
  const start = Date.now();
  while (Date.now() - start < 200) {}
  res.end('done');
});

server.listen(3000, () => {
  console.log('Server running on port 3000');
});
▶ Try it Yourself

Each request blocks the event loop for 200 ms, during which time no other requests can be processed.

▶ Example: Breaking Down Long-Running Tasks with setImmediate

JAVASCRIPT
function chunkedSort(arr, compareFn, callback) {
  const CHUNK = 1000;
  let offset = 0;

  function sortChunk() {
    const end = Math.min(offset + CHUNK, arr.length);
    for (let i = offset; i < end; i++) {
      for (let j = i + 1; j < end; j++) {
        if (compareFn(arr[i], arr[j]) > 0) {
          [arr[i], arr[j]] = [arr[j], arr[i]];
        }
      }
    }
    offset = end;
    if (offset < arr.length) {
      setImmediate(sortChunk);
    } else {
      callback(arr);
    }
  }

  setImmediate(sortChunk);
}

chunkedSort(
  [5, 3, 8, 1, 9, 2, 7, 4, 6],
  (a, b) => a - b,
  (result) => {
    console.log('Sorted:', result);
  }
);
▶ Try it Yourself
Dimension Blocking Code Non-blocking Code
Calculation Method Complete in a single synchronous loop Split into small chunks and execute sequentially using setImmediate
Event loop Stuck; other requests are on hold Control is yielded between slices; other callbacks can execute
Response Time Cumulative Delay Variation in Delay (User-Acceptable)
CPU Utilization Single-core 100% (spikes) Distributed usage, allowing time for I/O
Use Cases Very Short Computations Long Computations, High-Concurrency Services


7. Comprehensive Example: Script to Verify the Execution Order of the Event Loop

The following script verifies the complete execution order of setTimeoutprocess.nextTicksetImmediatePromise.then → I/O callback.

JAVASCRIPT
const fs = require('fs');

console.log('--- Script start ---');

setTimeout(() => {
  console.log('1. setTimeout (macrotask - timers)');
}, 0);

setImmediate(() => {
  console.log('2. setImmediate (macrotask - check)');
});

process.nextTick(() => {
  console.log('3. process.nextTick (microtask - highest)');
});

Promise.resolve().then(() => {
  console.log('4. Promise.then (microtask)');
});

fs.readFile(__filename, () => {
  console.log('5. I/O callback (poll phase)');

  setTimeout(() => {
    console.log('6.   inner setTimeout');
  }, 0);

  setImmediate(() => {
    console.log('7.   inner setImmediate');
  });

  process.nextTick(() => {
    console.log('8.   inner nextTick');
  });

  Promise.resolve().then(() => {
    console.log('9.   inner Promise.then');
  });
});

console.log('--- Script end ---');
TEXT 📖 Display only
--- Script start ---
--- Script end ---
3. process.nextTick (microtask - highest)
4. Promise.then (microtask)
1. setTimeout (macrotask - timers)
2. setImmediate (macrotask - check)
5. I/O callback (poll phase)
8.   inner nextTick
9.   inner Promise.then
7.   inner setImmediate
6.   inner setTimeout

Analysis of Execution Order:

  1. The synchronous code runs first, outputting "Script start" and "Script end"
  2. Clear microtasks after the current phase ends: nextTick → Promise.then
  3. Enter the "timers" phase: setTimeout
  4. Enter the check phase: setImmediate
  5. Execute the callback during the poll phase after I/O is complete
  6. Clear the microtask after the I/O callback completes: inner nextTick → inner Promise.then
  7. When the poll ends, enter the check: inner setImmediate
  8. Next round of timers: inner setTimeout

❓ FAQ

Q Does setTimeout(fn, 0) really execute after 0 ms?
A No. Both browsers and Node.js have a minimum delay (approximately 1 ms), and execution must wait until the event loop enters the "timers" phase; the actual wait time depends on the current phase.
Q Which runs first, process.nextTick or Promise.then?
A process.nextTick runs first. nextTick has the highest priority in the microtask queue, and the nextTick queue is always cleared before the Promise queue.
Q Why does setImmediate execute before setTimeout in an I/O callback?
A I/O callbacks are executed during the poll phase. Once that phase ends, the process moves directly to the check phase (setImmediate) before proceeding to the next round of the timers phase (setTimeout). Therefore, in an I/O context, setImmediate always executes before setTimeout.
Q Is the event loop single-threaded or multi-threaded?
A JavaScript execution is single-threaded, but the underlying libuv has a thread pool (4 threads by default) that handles operations such as DNS lookups and file I/O. Network I/O is handled by the operating system kernel, which notifies the event loop once it is complete.
Q How can you avoid blocking the event loop?
A Break long computations into smaller chunks and execute them step by step using setImmediate; use worker threads to move CPU-intensive tasks to separate threads; use asynchronous I/O instead of synchronous I/O; and use streams to process large datasets in chunks.
Q Can process.nextTick be called recursively indefinitely?
A It is not recommended. Node.js has a default limit set by process.maxTickDepth; recursive calls to nextTick will prevent the event loop from entering the next phase, leading to I/O starvation. You should use setImmediate instead of recursive nextTick calls.

📖 Summary


📝 Exercises

  1. Complete all the code examples in this lesson and make sure each one runs correctly.
  2. Modify the comprehensive example and add your own extensions
  3. Review the official documentation, identify 1–2 APIs not covered in this lesson, and write test code for them.
  4. Reflection: How would you apply what you’ve learned in this lesson to a real-world project?
  5. Try to combine what you’ve learned in this lesson with material from previous lessons to build a small project.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏