What is the Event Loop?
The event loop is what allows Node.js to perform non-blocking I/O operations despite JavaScript being single-threaded. It offloads operations to the system kernel whenever possible and handles callbacks when operations complete.
🔄 Event Loop Phases
1
Timers - Execute setTimeout() and setInterval() callbacks
2
Pending callbacks - I/O callbacks deferred from previous loop
3
Idle, prepare - Internal use only
4
Poll - Retrieve new I/O events, execute I/O callbacks
5
Check - Execute setImmediate() callbacks
6
Close callbacks - socket.on('close'), etc.
Execution Order Example
console.log('1: Script start');
setTimeout(() => {
console.log('2: setTimeout');
}, 0);
setImmediate(() => {
console.log('3: setImmediate');
});
Promise.resolve().then(() => {
console.log('4: Promise');
});
process.nextTick(() => {
console.log('5: nextTick');
});
console.log('6: Script end');
// Output:
// 1: Script start
// 6: Script end
// 5: nextTick (microtask queue - highest priority)
// 4: Promise (microtask queue)
// 2: setTimeout (timers phase)
// 3: setImmediate (check phase)
Microtasks vs Macrotasks
| Microtasks (High Priority) | Macrotasks (Lower Priority) |
|---|---|
| process.nextTick() | setTimeout() |
| Promise.then/catch/finally | setInterval() |
| queueMicrotask() | setImmediate() |
| I/O operations |
Microtasks are processed after each phase, before moving to the next phase. All pending microtasks are drained before continuing.
process.nextTick() vs setImmediate()
// process.nextTick - runs BEFORE the event loop continues
// setImmediate - runs in the CHECK phase of the event loop
// Inside an I/O callback, setImmediate runs first
const fs = require('fs');
fs.readFile('file.txt', () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
// Output: immediate, timeout
// Outside I/O, order is non-deterministic
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// Output: could be either order!
// nextTick always runs first
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
// Output: nextTick, promise
Blocking the Event Loop
⚠️ Long-running synchronous code blocks the entire event loop!
// BAD: Blocks the event loop
function heavyComputation() {
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
}
return sum;
}
// All requests are blocked during computation
const http = require('http');
http.createServer((req, res) => {
const result = heavyComputation(); // Blocks for seconds!
res.end(String(result));
}).listen(3000);
// BETTER: Break work into chunks
function heavyComputationAsync(callback) {
let sum = 0;
let i = 0;
const batchSize = 1e6;
function processBatch() {
const end = Math.min(i + batchSize, 1e9);
while (i < end) {
sum += i++;
}
if (i < 1e9) {
// Yield to event loop
setImmediate(processBatch);
} else {
callback(sum);
}
}
processBatch();
}
// BEST: Use Worker Threads (next lesson!)
Async Patterns and the Event Loop
// Understanding async execution order
async function example() {
console.log('1: async start');
await Promise.resolve();
console.log('2: after await');
return 'done';
}
console.log('3: before call');
example().then(r => console.log('4:', r));
console.log('5: after call');
// Output:
// 3: before call
// 1: async start
// 5: after call
// 2: after await
// 4: done
// Why? 'await' schedules continuation as a microtask
Event Loop Monitoring
// Measure event loop lag
let lastCheck = Date.now();
setInterval(() => {
const now = Date.now();
const lag = now - lastCheck - 1000; // Expected 1000ms
if (lag > 100) {
console.warn('Event loop lag:', lag, 'ms');
}
lastCheck = now;
}, 1000);
// Using monitorEventLoopDelay (Node.js 11+)
const { monitorEventLoopDelay } = require('perf_hooks');
const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();
setInterval(() => {
console.log('Event loop delay:');
console.log(' Min:', histogram.min / 1e6, 'ms');
console.log(' Max:', histogram.max / 1e6, 'ms');
console.log(' Mean:', histogram.mean / 1e6, 'ms');
console.log(' P99:', histogram.percentile(99) / 1e6, 'ms');
histogram.reset();
}, 5000);
Common Pitfalls
// 1. Recursive nextTick can starve the event loop
function badRecursion() {
process.nextTick(badRecursion); // Never yields!
}
// 2. Creating too many timers
// BAD
for (let i = 0; i < 10000; i++) {
setTimeout(() => {}, Math.random() * 1000);
}
// BETTER: Use a scheduling library or batch operations
// 3. Mixing sync and async in callbacks
function problematic(callback) {
if (cache[key]) {
callback(cache[key]); // Sync!
} else {
fetchData(key, callback); // Async!
}
}
// FIXED: Always be async
function fixed(callback) {
if (cache[key]) {
process.nextTick(() => callback(cache[key]));
} else {
fetchData(key, callback);
}
}
💡 Key Takeaways
- • The event loop processes phases in order: timers → poll → check → close
- • Microtasks (nextTick, Promises) run between each phase
- • process.nextTick() has higher priority than Promises
- • Never block the event loop with synchronous operations
- • Use setImmediate() to yield to the event loop