Suggest an editImprove this articleRefine the answer for “How the Event Loop works in Node.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Event Loop** is the mechanism that lets single-threaded Node.js handle concurrent I/O without blocking. It routes callbacks through six ordered phases (timers, pending, poll, check, close), draining `process.nextTick` and Promise microtasks between every phase transition. ```javascript setTimeout(() => console.log('timer'), 0); // timers phase setImmediate(() => console.log('immediate')); // check phase Promise.resolve().then(() => console.log('promise')); // microtask process.nextTick(() => console.log('nextTick')); // microtask (first) // Output: nextTick -> promise -> timer -> immediate ``` **Key:** blocking the main thread with CPU work (long loops, `fs.readFileSync`) prevents the event loop from processing any other callbacks.Shown above the full answer for quick recall.Answer (EN)Image**Event Loop** is the mechanism that lets single-threaded Node.js handle concurrent I/O without blocking. It delegates work to the OS and libuv, then routes completed callbacks through six ordered phases per tick. ## Theory ### TL;DR - Node.js is single-threaded but non-blocking because the event loop offloads I/O to libuv and the OS, then picks results up when they are ready - Think of it like a waiter who places kitchen orders and keeps serving other tables instead of standing at the pass waiting - Six phases per tick, fixed order: timers, pending callbacks, idle/prepare, poll, check, close - Microtasks (`process.nextTick` and Promises) drain completely between every phase transition, with `nextTick` ahead of Promises - Block the main thread with a CPU loop and the entire phase cycle stalls ### Quick example ```javascript const fs = require('fs'); console.log('1: sync'); process.nextTick(() => console.log('2: nextTick')); // microtask, highest priority Promise.resolve().then(() => console.log('3: promise')); // microtask setTimeout(() => console.log('4: timer'), 0); // timers phase setImmediate(() => console.log('5: immediate')); // check phase fs.readFile(__filename, () => { setImmediate(() => console.log('6: I/O immediate')); // check, before next timers setTimeout(() => console.log('7: I/O timer'), 0); }); // Output: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 // Inside the I/O callback, 6 always comes before 7 ``` Sync runs first. Then microtasks. Then the loop cycles through phases. Inside an I/O callback, `setImmediate` always fires before `setTimeout(fn, 0)` because the check phase follows poll directly. ### How the event loop runs V8 executes JavaScript synchronously on the main thread. When code calls `fs.readFile` or makes a network request, Node.js hands that work to libuv, a C library that ships with Node. libuv uses kernel-level async APIs (epoll on Linux, kqueue on macOS) for network I/O, and its own thread pool (4 threads by default, configurable via `UV_THREADPOOL_SIZE`) for file system, DNS, and crypto operations. When an operation completes, libuv places the callback in the right phase queue. The event loop cycles through those queues in fixed order. One full cycle is called a tick. ### The six phases of a tick ``` ┌───────────────────────────┐ ┌─>│ timers │ <- setTimeout, setInterval callbacks │ └─────────────┬─────────────┘ │ │ <- microtasks (nextTick + Promises) │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ <- I/O callbacks deferred from last tick │ └─────────────┬─────────────┘ │ │ <- microtasks │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ <- internal use only (libuv) │ └─────────────┬─────────────┘ │ │ <- microtasks │ ┌─────────────┴─────────────┐ │ │ poll │ <- retrieve new I/O events, execute I/O callbacks │ └─────────────┬─────────────┘ │ │ <- microtasks │ ┌─────────────┴─────────────┐ │ │ check │ <- setImmediate callbacks │ └─────────────┬─────────────┘ │ │ <- microtasks │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ <- socket.on('close'), stream.on('close') └───────────────────────────┘ ``` **Timers:** Runs `setTimeout` and `setInterval` callbacks whose delay has expired. `setTimeout(fn, 0)` means "run at the next timers phase, after at least 0ms" - not "run immediately." Under load, it often arrives later. **Pending callbacks:** I/O callbacks deferred from the previous iteration. Certain TCP error notifications from the OS land here. **Poll:** The center of the loop. Node.js retrieves completed I/O events from the OS and runs their callbacks. If the poll queue is empty and no `setImmediate` callbacks are registered, the loop waits here for new I/O - blocking intentionally. It moves on when a timer threshold is about to expire or a `setImmediate` was queued. **Check:** `setImmediate` callbacks run here. Because check follows poll directly, `setImmediate` registered inside an I/O callback fires before the timers phase of the next tick. **Close callbacks:** Cleanup handlers. `socket.destroy()` triggers `socket.on('close', ...)` in this phase. ### Microtask queue priority Between every phase transition, Node.js pauses to drain two queues in this order: 1. `process.nextTick` callbacks 2. Promise `.then()` callbacks ```javascript console.log('1: sync'); setTimeout(() => console.log('5: timer'), 0); setImmediate(() => console.log('6: immediate')); Promise.resolve().then(() => console.log('3: promise')); process.nextTick(() => console.log('2: nextTick')); console.log('4: sync end'); // Output: // 1: sync // 4: sync end // 2: nextTick <- nextTick queue drains first // 3: promise <- then Promise microtasks // 5: timer <- timers phase (next tick) // 6: immediate <- check phase (next tick) ``` `process.nextTick` belongs to no phase. It fires when the current operation ends, before any phase runs. That makes it useful for callbacks that must run "after this sync block but before any I/O." ### When to use - HTTP APIs and REST servers: event loop handles thousands of concurrent connections on one thread - Real-time apps with WebSockets: check phase keeps `setImmediate` emits fast - CPU-heavy work (image resizing, cryptography): move off the main thread using `worker_threads` - Iterative background tasks: use `setImmediate` over `process.nextTick` so I/O stays unblocked between iterations ### Common mistakes **Assuming `setTimeout(fn, 0)` runs right after current code:** ```javascript setTimeout(() => console.log('timer'), 0); process.nextTick(() => console.log('nextTick')); // Output: nextTick, then timer // nextTick drains before the loop even reaches the timers phase ``` **Blocking the loop with synchronous CPU work:** ```javascript // Freezes all other requests until this finishes app.get('/compute', (req, res) => { let sum = 0; for (let i = 0; i < 1e9; i++) sum += i; res.send(String(sum)); }); // Fix: offload to worker_threads const { Worker } = require('worker_threads'); app.get('/compute', (req, res) => { const worker = new Worker('./compute-worker.js'); worker.on('message', (result) => res.send(String(result))); }); ``` **Recursive `process.nextTick` starves I/O:** ```javascript // Dangerous: everything waits until this finishes function recurse(n) { if (n === 0) return; process.nextTick(() => recurse(n - 1)); } recurse(1000000); // I/O, timers, setImmediate all blocked // Safe: yield to the loop each iteration function recurse(n) { if (n === 0) return; setImmediate(() => recurse(n - 1)); } ``` **Non-deterministic order of `setImmediate` and `setTimeout` outside I/O:** ```javascript // Order is timing-dependent here setTimeout(() => console.log('timeout'), 0); setImmediate(() => console.log('immediate')); // Order is guaranteed here fs.readFile('/dev/null', () => { setTimeout(() => console.log('timeout'), 0); setImmediate(() => console.log('immediate')); // always first }); ``` This one trips up seniors in interviews. The rule: inside an I/O callback, `setImmediate` always wins. Outside, the order depends on whether the timer threshold expired before the loop started its current tick. I ran into the non-deterministic case in production once - a script assumed `setImmediate` always fired first, but it was registered outside any I/O callback. It passed locally and failed intermittently on the CI server. ### Real-world usage - Express.js: route handler callbacks run in the poll phase; DB queries hand off to libuv and return in a later poll iteration - Socket.io: event emits queue in the check phase, keeping latency low without blocking new I/O - Fastify: plugin initialization hooks into async loop phases to avoid blocking startup - PM2 cluster mode: spawns one Node process per CPU core, each with its own event loop - Node.js streams: readable/writable data events arrive through poll, enabling backpressure without blocking ### Follow-up questions **Q:** What are the six event loop phases in order? **A:** Timers, pending callbacks, idle/prepare, poll, check, close. Microtasks (nextTick first, then Promises) drain between each transition. **Q:** Why does `setImmediate` beat `setTimeout(fn, 0)` inside an I/O callback? **A:** Inside an I/O callback the loop is in the poll phase. Next comes check, where `setImmediate` runs. The timers phase only arrives in the next tick. **Q:** What happens if `process.nextTick` calls itself recursively? **A:** The event loop never advances. All I/O callbacks, timers, and `setImmediate` calls block until the recursion ends. The Node.js docs explicitly warn against this. **Q:** What does libuv's thread pool actually handle? **A:** File system operations, DNS lookups, and crypto work. Network I/O does not go through the thread pool - it uses kernel async APIs directly. Default pool size is 4 threads, set `UV_THREADPOOL_SIZE` to change it. **Q:** How is Node's event loop different from the browser's? **A:** Browsers process tasks and microtasks but have no explicit phases like check or close. `setImmediate` is Node-only. Browsers also insert rendering steps between task batches. **Q:** Senior question: you call `setImmediate` inside a poll callback. How many loop ticks pass before it runs? **A:** Zero extra ticks. Check follows poll in the same tick. Microtasks between the two phases drain first, then the `setImmediate` callback runs. A second `setImmediate` registered inside that callback runs in the check phase of the next tick. ## Examples ### Basic: async execution order ```javascript console.log('A'); setTimeout(() => console.log('D: setTimeout'), 0); Promise.resolve().then(() => console.log('C: Promise')); process.nextTick(() => console.log('B: nextTick')); console.log('E: sync end'); // Output: // A // E: sync end // B: nextTick <- microtask, runs before any phase // C: Promise <- microtask, after nextTick queue // D: setTimeout <- timers phase, next tick ``` Sync code finishes first (A, E). Then the nextTick queue (B). Then Promise microtasks (C). Only then does the loop enter the timers phase (D). ### setImmediate vs setTimeout inside and outside I/O ```javascript const fs = require('fs'); // Outside I/O callback - order is non-deterministic setTimeout(() => console.log('outer timeout'), 0); setImmediate(() => console.log('outer immediate')); // Either order is valid here // Inside I/O callback - order is fixed fs.readFile('/dev/null', () => { setTimeout(() => console.log('inner timeout'), 0); setImmediate(() => console.log('inner immediate')); // Always: inner immediate -> inner timeout }); ``` Outside an I/O callback, `setTimeout` may have already passed the timers phase before `setImmediate` was even registered. Inside an I/O callback the loop is in poll, and check comes next. `setImmediate` wins, every time. ### Starving the loop with recursive nextTick ```javascript // Dangerous: all I/O, timers, and setImmediate wait function recursiveNextTick(count) { if (count === 0) return; process.nextTick(() => recursiveNextTick(count - 1)); } recursiveNextTick(1000000); // Safe: setImmediate lets the loop breathe function recursiveImmediate(count) { if (count === 0) return; setImmediate(() => recursiveImmediate(count - 1)); } ``` `process.nextTick` was built for small one-off deferrals within a phase. `setImmediate` is the right tool when you need to iterate in the background without blocking the server. The difference in production is a server that stays responsive vs one that queues all incoming requests until the recursion finishes.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.