One of the most common questions asked in backend interviews is: "If Node.js is single-threaded, how does it handle thousands of concurrent connections?" The answer lies in the Event Loop. Understanding this concept is crucial for writing performant Node.js applications and avoiding blocking code.
It is true that JavaScript executes on a single main thread. However, Node.js itself is not entirely single-threaded. It uses the C++ library libuv to handle asynchronous I/O operations. When Node.js needs to perform a heavy task (like reading a file or making a network request), it offloads that work to libuv's thread pool, allowing the main thread to continue executing other code.
The event loop is essentially an infinite loop that waits for tasks, executes them, and then sleeps until more tasks arrive. It operates in several distinct phases:
- Timers: Executes callbacks scheduled by
setTimeout()andsetInterval(). - Pending Callbacks: Executes I/O callbacks deferred to the next loop iteration.
- Idle, Prepare: Used internally by Node.js.
- Poll: Retrieves new I/O events; executes I/O related callbacks (almost all with the exception of close callbacks, timers, and
setImmediate()). - Check: Executes
setImmediate()callbacks. - Close Callbacks: Executes close events (e.g.,
socket.on('close', ...)).
Because there is only one thread executing JavaScript, if you write a CPU-intensive operation (like a massive while loop or heavy cryptography) on the main thread, the Event Loop will freeze. No other requests can be handled while this operation completes.
fs.readFile instead of fs.readFileSync) when building a web server to ensure the Event Loop remains free to handle incoming traffic.
Once you understand how the Event Loop delegates tasks to the OS kernel and thread pool, you'll be able to design highly scalable backend systems in Node.js.