Understanding the Node.js Event Loop: Phases, Microtasks, and Blocking Explained
The event loop is what lets single-threaded Node.js handle thousands of connections at once: instead of waiting on slow operations, it hands them off and keeps working, running your callbacks when the results are ready. Understanding its phases is the difference between an app that scales and one that mysteriously stalls.
The phases, in order
Each turn of the loop moves through phases, each with its own callback queue:
- Timers — runs callbacks from
setTimeoutandsetInterval. - Pending callbacks — certain deferred system callbacks.
- Poll — retrieves new I/O events and runs their callbacks (the loop spends most time here).
- Check — runs
setImmediatecallbacks. - Close — handles close events like a socket closing.
Where microtasks fit
Between operations, Node drains two microtask queues: the process.nextTick queue first, then the Promise queue. These run before the loop moves on, so a flood of process.nextTick calls can starve the loop. For the exact ordering, see nextTick vs setImmediate vs setTimeout.
The cardinal sin: blocking the loop
Because it is single-threaded, any long synchronous operation freezes everything — no other request is served until it finishes. Common offenders:
- Heavy loops or large
JSON.parseon huge payloads. - Synchronous file or crypto calls (the
...Syncversions). - CPU-bound work like image processing or complex calculations.
The fixes: use asynchronous APIs, break big jobs into chunks, and offload CPU-heavy work to worker threads.
Frequently asked questions
Is Node.js really single-threaded?
Your JavaScript runs on one main thread, but Node uses a background thread pool (libuv) for I/O. So your code is single-threaded while I/O happens in parallel underneath — which is why blocking that one thread is so damaging.
How do I know if I'm blocking the event loop?
Symptoms include requests hanging under load and rising latency. You can measure event-loop lag with monitoring tools — see monitoring a production Node.js app.
Should I use setImmediate or setTimeout(fn, 0)?
Prefer setImmediate when you want to yield after the current poll phase. Their exact ordering differs by context — see the dedicated timing guide.
Was this article helpful?