Chapter 22 — The Event Loop
📖 Definition
The event loop is Node.js's mechanism for performing non-blocking I/O on a single-threaded JavaScript engine by offloading work to the OS or thread pool and handling completion callbacks in phases.
🔍 Explanation
Node.js runs JS on a single main thread. It achieves concurrency by:
- Using OS-level async I/O (epoll/kqueue/IOCP).
- Using a thread pool (libuv) for fs, dns, crypto.
- Polling completed work in a loop and running JS callbacks one at a time.
The Six Phases (simplified)
┌───────────────────────────┐
┌─>│ timers │ ← setTimeout / setInterval callbacks
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ ← deferred TCP errors, etc.
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ ← internal
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │ ← retrieve new I/O; run their callbacks
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │ ← setImmediate callbacks
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │ ← socket.on('close'), etc.
└───────────────────────────┘Between every phase, the microtask queue drains: process.nextTick callbacks first, then resolved Promise callbacks.
💻 Code Example — Microtask vs Macrotask
console.log("1");
setTimeout(() => console.log("2"), 0); // macrotask (timers phase)
setImmediate(() => console.log("3")); // macrotask (check phase)
Promise.resolve().then(() => console.log("4")); // microtask
process.nextTick(() => console.log("5")); // microtask (highest priority)
console.log("6");
// Output: 1, 6, 5, 4, 2, 3
// (2 vs 3 ordering can vary — depends on the I/O context.)💻 Code Example — Blocking the Loop
const http = require("http");
http.createServer((req, res) => {
if (req.url === "/slow") {
// ❌ CPU-bound loop blocks every other request
let sum = 0;
for (let i = 0; i < 1e10; i++) sum += i;
return res.end(`Sum=${sum}`);
}
res.end("OK");
}).listen(3000);
// While /slow runs, /OK requests pile up and wait.Fix 1 — Offload to a Worker Thread
const { Worker } = require("worker_threads");
http.createServer((req, res) => {
if (req.url === "/slow") {
const worker = new Worker(`
const { parentPort } = require("worker_threads");
let sum = 0;
for (let i = 0; i < 1e10; i++) sum += i;
parentPort.postMessage(sum);
`, { eval: true });
worker.on("message", (sum) => res.end(`Sum=${sum}`));
}
});Fix 2 — Break the work into chunks
function expensiveAsync(callback) {
let i = 0, sum = 0;
function step() {
const end = i + 1e6;
for (; i < end && i < 1e10; i++) sum += i;
if (i < 1e10) setImmediate(step);
else callback(sum);
}
step();
}💻 Code Example — setImmediate vs setTimeout(0)
setImmediate(() => console.log("immediate"));
setTimeout(() => console.log("timeout"), 0);
// Inside an I/O callback, setImmediate runs first.
// At top level, order is non-deterministic.💻 Code Example — process.nextTick Caution
process.nextTick(function recurse() {
process.nextTick(recurse); // ❌ starves I/O
});
// nextTick queue drains completely before moving on — abuse = frozen server.💻 Code Example — Async I/O is Non-Blocking
const fs = require("fs/promises");
console.log("start");
fs.readFile("big.txt", "utf8").then(() => console.log("done"));
console.log("end");
// Output: start, end, done
// Reading happens off the main thread.💻 Code Example — Thread Pool Limit
// crypto.pbkdf2 uses the libuv thread pool (default 4 workers)
const crypto = require("crypto");
for (let i = 0; i < 8; i++) {
console.time(`hash-${i}`);
crypto.pbkdf2("pw", "salt", 100000, 64, "sha512", () => {
console.timeEnd(`hash-${i}`);
});
}
// First 4 finish around the same time; next 4 wait.
// Increase with: UV_THREADPOOL_SIZE=8 node app.js🌍 Real-World Impact
- Servers handle thousands of concurrent connections by avoiding sync work.
- Identify CPU-bound endpoints with profilers and move them to workers/queues.
- Tune
UV_THREADPOOL_SIZEfor crypto-heavy workloads.
🎯 Likely Interview Questions
- Explain the event loop.
- Why is Node.js single-threaded but still scalable?
- Difference between
setImmediate,setTimeout(0), andprocess.nextTick? - What happens if you put a
while(true)in a handler? — Blocks all other requests. - What is the libuv thread pool? — A pool of OS threads used for fs, crypto, dns, zlib. Default size 4.