Knowing the architecture is one thing — seeing it in motion is another. Let's trace what actually happens, step by step, when a Node.js server receives requests from multiple users at the same time.
The setup
Imagine a small server with one route: it looks up a product's price in a database and sends it back. Three users — call them A, B, and C — hit this route within a few milliseconds of each other.
Step by step
- Request A arrives. Node.js's single main thread picks it up and starts running the route's code.
- The code asks the database for the price. This is an I/O operation, so Node.js doesn't sit and wait — it hands the database query off (through libuv) and immediately becomes free again.
- Request B arrives, milliseconds later. Since the main thread isn't stuck waiting on A's database call, it's free to pick up B right away and start its own database query.
- Request C arrives. Same story — the main thread handles it, kicks off its own I/O, and moves on.
- Database results start coming back, not necessarily in the order the requests came in — maybe B's answer arrives first because that particular query was faster. The event loop notices B's callback is ready and runs it, sending B's response.
- A's and C's results follow whenever they're ready, each handled by the event loop in turn.
Notice what didn't happen: at no point did the server dedicate a separate thread to each user, and at no point did request B have to wait in a queue until request A was fully finished. The single thread stayed busy the entire time, but never blocked.
What happens with a CPU-heavy task
Now swap the database call for something that isn't I/O — say, calculating something computationally expensive directly on the main thread, like resizing a large image with pure JavaScript math. This does run on the main thread, and the main thread can only do one thing at a time. While that calculation runs, request B and C simply have to wait, even though they only wanted a fast, simple response. This is the classic Node.js pitfall: one heavy synchronous task can stall every other user connected to your server at that moment.
The practical takeaway
- I/O-bound work (databases, file reads, network calls, API requests) → Node.js shines, because that work is delegated away from the main thread.
- CPU-bound work (image processing, encryption of huge payloads, complex math) → best moved off the main thread entirely, using worker threads or a separate service, or the whole server's responsiveness suffers.
This single idea — delegate what can wait, protect the main thread from what can't — is really the core design philosophy behind everything Node.js does.