The event loop is THE most asked Node.js interview question. In simple language — it’s the mechanism that lets a single-threaded runtime handle thousands of concurrent operations without blocking.
When we call setTimeout, fs.readFile, or a network request, Node hands the work to libuv and our callback gets queued. The event loop is the orchestrator that picks the right callback to run next.
The 6 phases
The event loop runs in a loop (obviously), and each tick of that loop goes through 6 phases in order. Each phase has its own callback queue.
The four we actually care about in interviews are timers, poll, check, and close.
Microtasks run BETWEEN phases
Here’s the key bit most people miss. Microtasks aren’t a phase. They run between every phase (and after every individual callback). There are two microtask queues:
process.nextTickqueue — runs first, drained completely- Promise (
then/catch/finally) queue — runs second, drained completely
So the real flow is: run a callback → drain nextTick queue → drain Promise queue → next callback. This is why process.nextTick can starve the event loop if we recurse on it.
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
Promise.resolve().then(() => console.log("promise"));
process.nextTick(() => console.log("nextTick"));
console.log("sync");
// Output:
// sync
// nextTick
// promise
// timeout (or immediate first — depends on context)
// immediate
setImmediate vs setTimeout(fn, 0)
Classic interview trap. Outside of an I/O callback, the order is non-deterministic — it depends on how fast Node enters the loop. Inside an I/O callback, setImmediate ALWAYS runs first (because we’re already past the timers phase, heading to check).
const fs = require("node:fs");
fs.readFile(__filename, () => {
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
});
// Always: immediate, timeout
process.nextTick — use carefully
process.nextTick fires before any I/O or timer, after the current operation. It’s how we defer something to “right after this function but before anything else”.
Think of it like — “finish this stack, then immediately do this, before going back to the event loop”.
Recursive nextTick calls can block the loop forever. Recursive Promise resolutions have the same risk in newer Node (they share priority since Node 11+).
Why Node feels concurrent
While our JS is sync and single-threaded, libuv runs I/O on a 4-thread worker pool by default (configurable via UV_THREADPOOL_SIZE). File system ops, DNS lookups, and crypto use this pool. Network I/O uses the OS’s epoll/kqueue directly — no thread needed.
The thread pool finishes a job → pushes the callback into the right phase queue → event loop picks it up on the next tick.
Practical takeaway
- Don’t do heavy CPU work on the main thread — it blocks every phase.
process.nextTickfor “must run before any I/O”.setImmediatefor “run after current poll phase”.queueMicrotaskis the standard, cross-platform way to schedule a microtask (uses the Promise queue).- If our loop is lagging, check for sync code, big JSON.parse, or sync
fscalls.