Chapter 7: Node.js Event Loop
Node.js Event Loop — written as if I’m sitting next to you with a whiteboard, drawing phases, arrows, and little code snippets while explaining what really happens under the hood.
We will go slowly, step by step, with visuals in text form, real examples, and the most common misunderstandings people have.
1. The most important sentence you must remember
The Event Loop is what makes Node.js feel “non-blocking” and allows it to handle thousands of connections with only one JavaScript thread.
Node.js is single-threaded for JavaScript execution, but asynchronous thanks to the event loop + libuv.
Without the event loop → Node.js would behave like old-school PHP or blocking Python → one request at a time.
2. Very simplified mental model (first version)
Imagine you are a very fast waiter in a huge restaurant:
- You have only one brain (single JavaScript thread)
- You can take orders instantly (accept new connections)
- You give the order to the kitchen (file system, database, network…)
- While the kitchen is cooking → you immediately go to the next table
- When the kitchen rings the bell → you pick up the food and serve it
The event loop is your brain constantly checking:
“Do I have any finished dishes / timers / new customers right now?”
That checking loop never stops.
3. The real phases of the Node.js Event Loop
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
┌───────────────────────────────────────┐ │ Event Loop (one full tick) │ └──────────────────────┬────────────────┘ │ ┌──────────────────────▼───────────────────────┐ │ 1. Timers (setTimeout, setInterval) │ ├───────────────────────────────────────────────┤ │ 2. Pending Callbacks (most OS-level callbacks) │ ├───────────────────────────────────────────────┤ │ 3. Idle, Prepare (internal – rarely used) │ ├───────────────────────────────────────────────┤ │ 4. Poll ← the most important phase!│ ├───────────────────────────────────────────────┤ │ 5. Check (setImmediate callbacks) │ ├───────────────────────────────────────────────┤ │ 6. Close Callbacks (socket.on('close') etc.) │ └───────────────────────────────────────────────┘ |
Every time the event loop finishes one full cycle → it’s called one tick.
Most real work happens in phases 1, 4 and 5.
4. Detailed explanation of each phase
Phase 1 – Timers
Executes callbacks of expired setTimeout and setInterval.
|
0 1 2 3 4 5 6 7 8 9 10 |
setTimeout(() => { console.log("Timer 1 finished"); }, 0); console.log("I run first – synchronous"); |
Output:
|
0 1 2 3 4 5 6 7 |
I run first – synchronous Timer 1 finished |
→ even with 0 ms → timer callbacks never run immediately
Phase 4 – Poll (the heart of Node.js)
This phase does two main things:
- Retrieves new I/O events that are ready (finished file reads, incoming HTTP data, network responses…)
- Executes their callbacks — if there are any
If there is nothing to do in poll → Node.js will wait here (unless there are timers or setImmediate waiting)
Very important rule:
If the event loop is only waiting in the poll phase → Node.js keeps the process alive If there is nothing left to do (no timers, no pending I/O, no setImmediate) → process exits
Phase 5 – Check
Executes setImmediate callbacks.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
setImmediate(() => { console.log("setImmediate"); }); setTimeout(() => { console.log("setTimeout 0"); }, 0); console.log("sync"); |
Very common result (especially on Linux/macOS):
|
0 1 2 3 4 5 6 7 8 |
sync setImmediate setTimeout 0 |
→ setImmediate often wins over setTimeout(…, 0) when run inside I/O callbacks
5. Classic examples everyone should run once
Example 1 – Timers vs setImmediate
|
0 1 2 3 4 5 6 7 8 9 |
setTimeout(() => console.log("timeout"), 0); setImmediate(() => console.log("immediate")); console.log("sync code"); |
Possible outputs:
|
0 1 2 3 4 5 6 7 8 |
sync code immediate ← most common on many systems timeout |
or
|
0 1 2 3 4 5 6 7 8 |
sync code timeout immediate |
→ it depends on when the script started relative to the poll phase
Example 2 – Inside an I/O callback
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => console.log("timeout inside readFile"), 0); setImmediate(() => console.log("immediate inside readFile")); }); console.log("sync"); |
Almost always prints:
|
0 1 2 3 4 5 6 7 8 |
sync immediate inside readFile timeout inside readFile |
→ because readFile callback runs in poll phase → setImmediate is next phase
Example 3 – process.nextTick (special case – NOT part of event loop phases)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
console.log("1 sync"); process.nextTick(() => console.log("2 nextTick")); console.log("3 sync"); setImmediate(() => console.log("4 setImmediate")); |
Output:
|
0 1 2 3 4 5 6 7 8 9 |
1 sync 3 sync 2 nextTick 4 setImmediate |
→ process.nextTick runs immediately after current operation, before the event loop continues
→ it’s not part of the official phases → it’s a microtask queue (like Promises)
6. Microtasks vs Macrotasks (very important distinction)
Node.js has two kinds of async callbacks:
| Type | Examples | When do they run? | Priority |
|---|---|---|---|
| Microtasks | process.nextTick, Promise.then | After current operation, before next phase | Highest |
| Macrotasks | setTimeout, setImmediate, I/O | In the proper event loop phase | Lower |
Order in one tick:
|
0 1 2 3 4 5 6 7 8 |
1. Execute current synchronous code 2. Run all queued microtasks (nextTick + Promise chain) 3. Move to next event loop phase (timers → poll → check…) |
7. Summary table – Quick reference
| Phase / Mechanism | Typical callbacks | Runs when? | Can starve the loop? |
|---|---|---|---|
| process.nextTick | Microtasks | After current operation | Yes – very dangerous |
| Timers | setTimeout, setInterval | When timer expires | No |
| Poll | I/O callbacks (fs, net, http…) | When I/O events are ready | No |
| Check | setImmediate | After poll phase | No |
| Close callbacks | socket.on(‘close’), server.on(‘close’) | When resources close | No |
8. Practical rules of thumb (2025–2026 style)
- Use setImmediate when you want something to happen after current I/O, but before next timer
- Use process.nextTickvery sparingly — only when you really need something to happen right now (dangerous in loops)
- Prefer await / Promise over callbacks — cleaner and microtask-based
- Never put heavy synchronous CPU work in hot paths — it blocks the entire loop
- For long CPU tasks → use Worker Threads (they have their own event loop)
Want to go deeper into any part?
Popular next steps people usually ask:
- How Promises and async/await interact with the event loop
- Real debugging: printing phase names to understand order
- What happens with setTimeout(…, 0) vs setImmediate in production
- How Worker Threads have their own event loops
- Common starvation patterns and how to avoid them
- Visualizing event loop with clinic.js or async_hooks
Tell me which direction you want — I’ll continue with more code examples and concrete scenarios. 😄
