The Event Loop: Node.jsβs Secret Superpower π’
Imagine a very smart waiter in a busy restaurant. This waiter never sleeps. He takes orders, delivers food, and handles everythingβall by himself! But hereβs the magic: he NEVER waits. If the kitchen is cooking your food, he goes to help other customers instead of standing around.
That waiter is the Event Loop.
Node.js has only ONE main waiter (one thread), but heβs so clever that he can serve thousands of customers at once!
What is the Event Loop? π
Think of the Event Loop as a merry-go-round that never stops spinning.
βββββββββββββββββββββββββββ
β Your Code Runs β
β "Hey, read this file!" β
ββββββββββββ¬βββββββββββββββ
β
βββββββββββββββββββββββββββ
β Event Loop Catches β
β "I'll handle that!" β
ββββββββββββ¬βββββββββββββββ
β
βββββββββββββββββββββββββββ
β Work Gets Done β
β (File is being read) β
ββββββββββββ¬βββββββββββββββ
β
βββββββββββββββββββββββββββ
β Callback Runs β
β "Here's your file!" β
βββββββββββββββββββββββββββ
Simple Example:
console.log('Order placed!');
setTimeout(() => {
console.log('Food is ready!');
}, 1000);
console.log('Waiting...');
Output:
Order placed!
Waiting...
Food is ready! β (after 1 second)
The Event Loop didnβt wait for the food. It kept going!
The Six Phases of the Event Loop π‘
Our merry-go-round has six stations. Each time it spins, it visits every station in order.
graph TD A[β° Timers] --> B[π Pending Callbacks] B --> C[π§ Idle/Prepare] C --> D[π‘ Poll] D --> E[β Check] E --> F[πͺ Close Callbacks] F --> A
Phase 1: Timers β°
What happens here?
Callbacks from setTimeout() and setInterval() run here.
setTimeout(() => {
console.log('Timer done!');
}, 100);
Itβs like an alarm clock. When the time is up, the callback gets to run!
Phase 2: Pending Callbacks π
What happens here? Some system operations (like network errors) save their callbacks for later. They run here.
Think of it as a βsorry, couldnβt do it earlierβ pile.
Phase 3: Idle/Prepare π§
What happens here? Node.js does internal housekeeping. You donβt write code for this phase.
Itβs the waiter straightening his bowtie between orders!
Phase 4: Poll π‘
What happens here? This is the busiest station! Here, Node.js:
- Fetches new I/O events (files, network, etc.)
- Runs their callbacks
const fs = require('fs');
fs.readFile('menu.txt', (err, data) => {
console.log('Menu loaded!');
});
When the file is ready, this callback runs in the Poll phase.
Phase 5: Check β
What happens here?
Callbacks from setImmediate() run here.
setImmediate(() => {
console.log('Immediate!');
});
Phase 6: Close Callbacks πͺ
What happens here? When things close (like a socket disconnecting), their cleanup callbacks run here.
socket.on('close', () => {
console.log('Goodbye!');
});
process.nextTick(): The VIP Pass ποΈ
Remember our waiter? Well, process.nextTick() is like a VIP customer. They get served BEFORE the next phaseβno matter what!
setTimeout(() => {
console.log('Timer');
}, 0);
process.nextTick(() => {
console.log('nextTick');
});
console.log('Main');
Output:
Main
nextTick β VIP goes first!
Timer
Why does this happen?
process.nextTick() has its own special queue. After the current code finishes, Node.js checks this queue BEFORE moving to the next Event Loop phase.
graph TD A[Current Code Finishes] --> B{nextTick Queue?} B -->|Yes| C[Run All nextTick Callbacks] C --> B B -->|No| D[Move to Next Phase]
When to use it?
- When you need something to run RIGHT after the current code
- When you want to give users a chance to set up event handlers
function MyThing() {
process.nextTick(() => {
this.emit('ready');
});
}
setImmediate(): The βAfter This Phaseβ Pass π«
setImmediate() is different. It says: βRun me in the CHECK phase of the CURRENT loop cycle.β
setImmediate(() => {
console.log('Immediate');
});
setTimeout(() => {
console.log('Timeout');
}, 0);
console.log('Main');
Output (usually):
Main
Timeout β (might vary)
Immediate β (might vary)
Wait, why βmight varyβ?
Inside the main module, the order between setTimeout(0) and setImmediate can vary. But inside an I/O callback, setImmediate ALWAYS runs first:
const fs = require('fs');
fs.readFile('file.txt', () => {
setTimeout(() => {
console.log('Timeout');
}, 0);
setImmediate(() => {
console.log('Immediate');
});
});
Output (always):
Immediate β Always first in I/O!
Timeout
Thread Pool & libuv: The Kitchen Staff π¨βπ³
Remember our waiter analogy? The waiter is fast, but he canβt cook! For heavy work, he needs the kitchen staffβthe Thread Pool.
What is libuv?
libuv is the engine room of Node.js. It provides:
- The Event Loop
- The Thread Pool
- Async I/O operations
ββββββββββββββββββββββββββββββ
β Node.js β
β ββββββββββββββββββββββββ β
β β Your Code β β
β ββββββββββββββββββββββββ β
β β β
β ββββββββββββββββββββββββ β
β β Event Loop β β
β β (libuv) β β
β ββββββββββββββββββββββββ β
β β β
β ββββββββββββββββββββββββ β
β β Thread Pool β β
β β (4 workers) β β
β ββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββ
What uses the Thread Pool?
| Uses Thread Pool π¨βπ³ | Uses OS directly π₯οΈ |
|---|---|
| File system (fs) | Network I/O |
| DNS lookups | TCP/UDP sockets |
| Crypto operations | Pipes |
| Compression (zlib) | TTY |
Thread Pool Size
By default, you get 4 threads. You can change this:
// Must set before requiring anything!
process.env.UV_THREADPOOL_SIZE = 8;
Max: 1024 threads (but be careful!)
Example: Thread Pool in Action
const crypto = require('crypto');
const start = Date.now();
// 4 hash operations - uses thread pool
for (let i = 0; i < 4; i++) {
crypto.pbkdf2('password', 'salt', 100000,
64, 'sha512', () => {
console.log(`Hash ${i+1}: ${Date.now()-start}ms`);
});
}
Output (on 4-core machine):
Hash 1: 52ms
Hash 2: 52ms
Hash 3: 52ms
Hash 4: 53ms β All roughly same time!
They all finish together because 4 threads work in parallel!
Putting It All Together π§©
Letβs trace through a complete example:
console.log('1: Start');
setTimeout(() => console.log('2: Timeout'), 0);
setImmediate(() => console.log('3: Immediate'));
process.nextTick(() => console.log('4: nextTick'));
Promise.resolve().then(() => console.log('5: Promise'));
console.log('6: End');
Output:
1: Start
6: End
4: nextTick β nextTick queue (VIP)
5: Promise β Microtask queue
2: Timeout β Timers phase
3: Immediate β Check phase
Why this order?
- Synchronous code runs first (1, 6)
- nextTick queue empties (4)
- Microtask queue empties (5 - Promises)
- Event Loop phases begin (2, 3)
Quick Reference Chart π
| Feature | When it runs | Use case |
|---|---|---|
process.nextTick() |
Before next phase | Immediate priority |
Promise.then() |
After nextTick | Async operations |
setTimeout(fn, 0) |
Timers phase | Delayed execution |
setImmediate() |
Check phase | After I/O |
The Big Picture πΌοΈ
graph TD A[Your Code] --> B[nextTick Queue] B --> C[Microtask Queue] C --> D[Event Loop] D --> E[Timers] E --> F[Pending] F --> G[Idle] G --> H[Poll] H --> I[Check] I --> J[Close] J --> E H --> K[Thread Pool] K --> H
Remember This! π§
- Event Loop = The smart waiter who never waits
- 6 Phases = Six stations on a merry-go-round
- nextTick = VIP pass (runs before next phase)
- setImmediate = After I/O, guaranteed in Check phase
- Thread Pool = Kitchen staff for heavy work (4 workers by default)
- libuv = The engine that powers everything
You now understand how Node.js handles thousands of requests with just one thread. Thatβs the magic of the Event Loop! π