One thread, many requests
Node runs your JavaScript on a single thread: one line at a time, on one call stack (the pile of currently-running functions).
Understanding this machinery is not optional trivia: it explains why one slow piece of code can freeze every user of a Node server (a real and common production failure), and it is the single most asked Node interview topic.
So how does one thread serve thousands of users? With the event loop:
- Your code runs to completion on the call stack.
- Slow things (timers, file reads, network calls) are handed to Node's internals, which wait outside the thread.
- When a slow thing finishes, its callback is placed in the callback queue.
- Whenever the call stack is empty, the event loop takes the next callback from the queue and runs it.
That is the whole trick. Nothing ever interrupts running code, and no code ever waits idle on the thread.
A zero-millisecond timer still waits
Even a 0 ms timer must sit in the callback queue until the current code finishes.
console.log("A. start handling request"); setTimeout(() => { console.log("C. database replied"); }, 0); console.log("B. request handler finished");
Output
A. start handling request B. request handler finished C. database replied
The 0 ms callback goes to the queue, and the queue is only read when the stack is empty, so B prints before C. The delay is a lower bound on when the callback may run, never a promise about when it will.
This is the practical meaning of "nothing ever interrupts running code". However long the handler takes, it finishes before any queued callback starts, so no callback can observe the handler halfway through its work.
That guarantee is the reason Node code rarely needs locks. Two callbacks cannot run at the same instant, so a shared counter or cache cannot be corrupted mid-update the way it can in a multithreaded server.
The flip side is the trap of setTimeout(fn, 0) as a fix for ordering problems. It reliably pushes work after the current stack and says nothing about how long the queue ahead of it is, so it delays by an unknown amount rather than by zero.
What a ten-second computation does to everyone else
If a request handler computes primes for 10 full seconds, the other users of your server all wait, because the single thread is busy and the event loop cannot run any callbacks.
The event loop can only hand out callbacks when the call stack is empty. A 10-second computation keeps the stack occupied, so every timer, request, and database reply waits its turn behind it.
The symptoms are distinctive and easy to misdiagnose. Health checks time out, unrelated endpoints appear broken, and the CPU sits at 100 percent on one core, and none of it points at the endpoint actually responsible.
The rule of thumb is that Node is great at waiting, meaning I/O, and bad at heavy computing on the main thread. The two are not the same kind of slow, and only the first kind is what non-blocking solves.
| Kind of slow | Blocks other users |
|---|---|
| waiting on a database | no |
| reading a file with the async API | no |
| a 10-second loop | yes |
readFileSync on a huge file | yes |
| hashing a password with a high cost factor | yes |
The escapes for genuine CPU work are worth naming now, since unit 7 hits the password-hashing case directly. Heavy computation belongs in a worker thread, a separate process, or a background job queue, so the request thread stays free to keep answering.
Ordering two timers
Without reordering the statements, only the two delay numbers change, and timer B is scheduled first in the code while printing last.
setTimeout(() => { console.log("timer B"); }, 40); setTimeout(() => { console.log("timer A"); }, 10); console.log("sync 1"); console.log("sync 2");
Output
sync 1 sync 2 timer A timer B
The synchronous lines always beat the timers, and that part needs no help. Both setTimeout calls only schedule, so the file runs to its end before either callback can be considered.
Timer A gets the smaller delay, here 10 ms against 40 ms, which is what makes it run first despite being written second. Source order determines nothing about timer callbacks, and the delays determine everything.
Keeping the two delays clearly apart, by 30 ms or more, is what makes the order reliable rather than lucky. Two timers set 1 ms apart can fire in either order depending on how busy the machine is, which is exactly the kind of test that passes locally and fails in CI.
Equal delays are the one case with a defined answer, since timers with identical delays run in the order they were scheduled. Depending on that is still fragile, because it makes the output depend on a rule most readers of the code do not know.