NodeJs Event Loop
Deep Dive into the Node.js Event Loop
This document provides a comprehensive explanation of the Node.js Event Loop, including its phases, the difference between blocking and non-blocking I/O, the role of timers and immediates, how yielding works, and the implications of CPU-heavy tasks in Node.js.
1. What is the Event Loop?
Node.js is built on a single-threaded JavaScript runtime, but it handles concurrent operations efficiently using the event loop. The event loop allows Node.js to: - Offload expensive or slow I/O tasks (like file system or network requests) to the operating system via libuv - Continue executing JavaScript without waiting for these operations - Process completed I/O operations’ callbacks when they are ready
This is why Node.js is highly suitable for I/O-heavy applications.
2. Event Loop Phases
The Node.js event loop operates in multiple phases per iteration (tick):
- Timers Phase
- Executes callbacks scheduled by setTimeout and setInterval whose timers have expired.
- Pending Callbacks Phase
- Executes certain system-level I/O callbacks (e.g., errors from TCP).
- Idle/Prepare Phase
- Used internally by Node.js, not directly relevant for developers.
- Poll Phase
- Retrieves new I/O events.
- Executes I/O-related callbacks (e.g., fs.readFile).
- If the queue is empty, the event loop will wait for incoming I/O before moving on.
- Check Phase
- Executes setImmediate callbacks.
- This phase always runs immediately after poll.
- Close Callbacks Phase
- Executes close events (e.g., socket.on('close')).
3. Microtasks vs. Macrotasks
Apart from these phases, Node.js maintains a microtask queue: - process.nextTick - Promise callbacks (.then, .catch, .finally)
Microtasks run immediately after the currently executing code but before the event loop moves to the next phase. This means they can “jump the queue.”
4. Timers vs. Immediates
- setTimeout(fn, 0) → runs in the timers phase on the next tick, after the specified delay.
- setImmediate(fn) → runs in the check phase, immediately after the poll phase completes.
If both are scheduled inside an I/O callback, setImmediate typically executes before setTimeout.
5. Example Walkthrough
fs.readFile('file.txt', () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
console.log('file read complete');
});
Step-by-Step Execution:
- fs.readFile offloads work to libuv.
- When the file read finishes, its callback is queued in the poll phase.
- Inside the callback:
- setTimeout → queued in timers phase
- setImmediate → queued in check phase
- console.log('file read complete') executes immediately
- Next tick:
- timers phase → logs timeout
- check phase → logs immediate
👉 Output:
file read complete
timeout
immediate
6. Yielding the Event Loop
When handling CPU-heavy tasks (e.g., loops up to 1e9), the event loop can become blocked, preventing other requests from being processed.
Yielding with setImmediate
You can split large loops into smaller chunks and use setImmediate to yield back to the event loop:
function heavyTask(iterations, cb) {
let i = 0;
function nextChunk() {
const end = Math.min(i + 1e5, iterations);
while (i < end) i++;
if (i < iterations) {
setImmediate(nextChunk); // yield control
} else {
cb();
}
}
nextChunk();
}
👉 This prevents blocking other requests entirely, making the server more responsive.
Analogy: The chef analogy works well here — instead of finishing one huge order before serving the next, the chef alternates between customers, ensuring everyone is served gradually.
7. Blocking vs Non-Blocking I/O
- Blocking I/O: The thread waits until the operation finishes (e.g., reading a file synchronously).
- Non-Blocking I/O: The operation is offloaded; the thread continues running other tasks. When the operation finishes, its callback runs in the event loop.
Example:
// Blocking
const data = fs.readFileSync('file.txt');
console.log(data);
// Non-blocking
fs.readFile('file.txt', (err, data) => {
console.log(data);
});
console.log('This runs immediately');
8. CPU-Bound vs. I/O-Bound
- I/O-Bound Tasks → Non-blocking in Node.js (file reads, network calls, database queries)
- CPU-Bound Tasks → Blocking in Node.js (heavy loops, image processing, cryptography)
👉 Node.js is excellent for I/O-bound workloads but not ideal for CPU-heavy tasks.
9. Handling CPU-Intensive Tasks
Options to handle CPU-heavy operations: - Worker Threads → Run code in parallel threads - Child Processes → Separate processes for CPU work - Native Add-ons / WebAssembly → Offload to compiled code (C++, Rust, etc.) - External Services → Use another service/microservice for processing
10. Visualization
┌────────────────────────┐
│ Phase 1: timers │ → setTimeout / setInterval
└────────────────────────┘
↓
┌────────────────────────┐
│ Phase 2: pending cbs │ → system I/O callbacks
└────────────────────────┘
↓
┌────────────────────────┐
│ Phase 3: idle/prepare │ → internal use
└────────────────────────┘
↓
┌────────────────────────┐
│ Phase 4: poll │ → fs, sockets, HTTP responses
└────────────────────────┘
↓
┌────────────────────────┐
│ Phase 5: check │ → setImmediate
└────────────────────────┘
↓
┌────────────────────────┐
│ Phase 6: close cb │ → socket.on('close')
└────────────────────────┘
↓
(loop repeats)
11. Key Takeaways
- The event loop makes Node.js powerful for async I/O.
- Execution order is: timers → pending callbacks → idle/prepare → poll → check → close callbacks.
- Microtasks (Promises, process.nextTick) run immediately after the current task.
- setTimeout vs setImmediate → timers run in the timers phase, immediates in the check phase.
- For CPU-heavy work, yield the loop or use worker threads.
- Node.js is best for I/O-bound workloads, not CPU-intensive tasks.

