beginner19 min

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 macrotasks

Try 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.

Loading editor…

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.

Loading editor…

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.

Loading editor…

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

Knowledge check

1. How can a single-threaded Node server handle thousands of concurrent connections?
2. Why is a CPU-heavy synchronous computation more dangerous in Node than an I/O wait of the same duration?
3. What is the correct execution order for synchronous code, a microtask, and a macrotask (setTimeout) started together?

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.