"Node.js is single-threaded" — you've heard it a hundred times. So how does it handle thousands of requests at once without freezing?
The answer is the event loop. Once it clicks, a lot of Node "magic" suddenly makes sense. Let's break it down.
The one-line mental model
Node runs your JavaScript on one main thread, but hands off slow work (file reads, network calls, timers) to the system, and picks up the results later via the event loop. So one thread juggles thousands of connections — it just never sits around waiting.
Blocking vs non-blocking
// ❌ Blocking — nothing else runs until this file is read
const data = fs.readFileSync('big.txt')
// ✅ Non-blocking — Node starts the read and moves on
fs.readFile('big.txt', (err, data) => {
console.log('done reading')
})
console.log('this prints FIRST')
Output:
this prints FIRST
done reading
The second version doesn't wait — that's the whole point.
The phases (simplified)
Each loop iteration goes through phases, in order:
-
Timers —
setTimeout/setIntervalcallbacks - Poll — I/O callbacks (file, network)
-
Check —
setImmediatecallbacks - Close — cleanup callbacks
And between every phase, Node drains the microtask queue first.
Microtasks jump the line
process.nextTick() and resolved Promises are microtasks — they run before the loop moves to the next phase.
console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('4')
Output:
1
4
3 ← Promise (microtask) runs before setTimeout
2
Most people guess 1 2 4 3. If you understand why it's 1 4 3 2, you understand the event loop.
setTimeout vs setImmediate
Inside an I/O callback, setImmediate always fires before setTimeout(fn, 0):
fs.readFile('f.txt', () => {
setTimeout(() => console.log('timeout'), 0)
setImmediate(() => console.log('immediate'))
})
// → immediate, then timeout
The one rule that keeps Node fast
Never block the event loop. A heavy CPU task (huge loop, sync crypto, giant JSON parse) freezes everything, because it hogs the single thread. Offload it:
-
worker_threadsfor CPU-heavy work - the
clustermodule to use all cores - a queue/microservice for big jobs
TL;DR
- One JS thread + the event loop = non-blocking I/O
- Microtasks (nextTick, Promises) run before the next phase
-
setImmediatebeatssetTimeout(0)inside I/O - Don't block the loop with heavy CPU work
The event loop is one of the most common interview questions too — if you want the full set, I put together a 64-question Node.js interview guide here: https://asbackendinstitute.com/blog/nodejs-interview-questions/top-60-nodejs-interview-questions
I teach hands-on backend development (Node.js, Express, MongoDB, Redis) at AS Backend Institute. More practical guides like this if you're learning backend. 🚀
Top comments (0)