The Node.js Runtime Model: Event Loop and Non-Blocking I/O
Why Node can handle thousands of concurrent connections on one thread — and the one mistake (blocking that thread) that defeats the entire model.
What you'll learn
- Explain why Node's single-threaded event loop can still handle many concurrent connections
- Distinguish I/O-bound waiting from CPU-bound blocking work
- Predict the relative execution order of synchronous code, microtasks, and macrotasks
Explanation
Node.js runs your JavaScript on a single thread — yet a Node server routinely handles thousands of simultaneous connections without spawning a thread per connection the way some older server models did. The resolution to that apparent contradiction is the event loop combined with non-blocking I/O: when Node needs to do something slow — read a file, query a database, wait for a network response — it hands that work off to the underlying system (which manages it outside your JavaScript thread) and immediately moves on to other work, registering a callback to run once the slow operation finishes. Your single thread is never sitting idle waiting; it's processing whatever else is ready while I/O happens in the background.
This model has one sharp edge, and understanding it is the single most important practical consequence of "single-threaded": I/O-bound waiting is cheap; CPU-bound computation is not. Waiting on a database query costs nothing on your thread — it's handled elsewhere, and your thread serves other requests meanwhile. But a genuinely expensive synchronous computation (parsing a huge file synchronously, a slow nested loop, an unoptimized regular expression) runs on that one thread, and while it runs, nothing else can — every other request, every other connection, is completely stalled until that computation finishes. This is why a single slow, CPU-heavy synchronous operation in a Node server is a much more serious problem than the "just a bit slow" it would be in a language that gives each request its own thread — it doesn't just slow down that one request, it freezes every concurrent request the server is handling.
The event loop also processes work in a specific, predictable order worth knowing precisely: all synchronous code in the current call runs first, then microtasks (Promise .then() callbacks, queueMicrotask) drain completely before anything else runs, and only then does the loop move on to macrotasks (setTimeout, I/O callbacks). A setTimeout(fn, 0) does not run "immediately" — it runs only after all synchronous code and all pending microtasks have already completed, however many of those there are.
Example
The real execution order of synchronous code, a microtask, and a macrotask -- the exact ordering a Node server relies on.
console.log("1: synchronous");
setTimeout(() => console.log("4: macrotask (setTimeout)"), 0);
Promise.resolve().then(() => console.log("3: microtask (Promise.then)"));
console.log("2: synchronous");
// Output order: 1, 2, 3, 4 -- sync code first, then ALL microtasks, then macrotasksTry it yourself
Add a second Promise.then() microtask and predict where it lands in the output order before running.
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Guided exercise
Guided exercise
Write predictOrder() that returns an array of the labels in the ACTUAL order they would log, given: sync code logs 'sync', a setTimeout(fn, 0) logs 'timeout', and a Promise.resolve().then() logs 'microtask'. Do not use setTimeout/Promise yourself -- just return the array in the correct order based on the rule from this lesson.
Checks: returns the correct execution order
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Independent exercise
Independent exercise
Write classifyWork(description) that returns 'io-bound' for descriptions containing 'database', 'file', 'network', or 'query' (case-insensitive), and 'cpu-bound' for anything else (like 'sorting', 'parsing', 'computing') -- modeling the practical distinction from this lesson.
Checks: classifies a database query as I/O-bound · classifies sorting as CPU-bound · classifies a file read as I/O-bound
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Common mistakes
- Assuming Node's single-threaded model means it can only handle one request at a time — it handles many concurrent I/O-bound requests efficiently, as long as none of them block the thread with heavy computation.
- Running an expensive synchronous computation directly in a request handler, unknowingly freezing every other concurrent request on the same server.
- Assuming `setTimeout(fn, 0)` runs immediately, when it actually waits for all synchronous code and all pending microtasks to finish first.
Knowledge check
Takeaway
Node's single thread stays responsive because I/O waiting is handed off elsewhere — but CPU-bound synchronous work runs on that same thread and blocks everything else while it does, which is the model's one sharp edge.
Summary
This lesson covered why non-blocking I/O lets a single thread serve many concurrent connections, the real danger of CPU-bound blocking work, and the precise execution order of synchronous code, microtasks, and macrotasks.
References
Your notes
Notes save automatically.
Finished this lesson?
Mark it complete to track your progress and schedule a future review.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.