Why Node.js is Called Single-Threaded and How It Serves Millions of Requests
Introduction
Node.js is often described as a single-threaded environment, yet it is capable of handling millions of concurrent requests. This document explains the reasons behind this design and how Node.js achieves high concurrency through its architecture. Additionally, it provides a detailed overview of the event loop and a full example to illustrate these concepts.
Single-Threaded Event Loop
Main Thread
- Node.js operates on a single main thread to execute JavaScript code. This thread runs the event loop, which is responsible for managing asynchronous operations.
What is the Event Loop?
- The event loop is a core part of Node.js that allows it to perform non-blocking I/O operations despite being single-threaded. It enables the execution of code, collecting and processing events, and executing queued sub-tasks.
How the Event Loop Works
- Call Stack: The main thread has a call stack for executing code. When a function is called, it is added to the stack, and when it returns, it is removed from the stack.
- Event Queue: The event loop checks the event queue for tasks to execute. If the call stack is empty, the event loop will take the first task from the event queue and push it onto the call stack for execution.
- Microtask Queue: In addition to the event queue, there is a microtask queue (for promises and process.nextTick) that takes priority over the event queue. If the call stack is empty, the event loop will first execute all microtasks before processing the next event.
Event Loop Phases
The event loop runs in multiple phases, each with its own purpose:
- Timers: Executes callbacks scheduled by
setTimeout()
andsetInterval()
. - I/O Callbacks: Executes callbacks for I/O operations (e.g., network operations, file system).
- Idle, Prepare: Internal phase, not typically interacted with directly.
- Poll: Retrieves new I/O events, executes their callbacks, and may block if no events are pending.
- Check: Executes callbacks scheduled by
setImmediate()
. - Close Callbacks: Executes close event callbacks (e.g., socket.on('close', ...)).
Non-Blocking I/O
Asynchronous Operations
- When Node.js performs I/O operations (e.g., reading from a file or querying a database), it does not wait for the operation to complete. Instead, it initiates the operation and continues executing other code.
Callbacks and Promises
- Upon completion of an I/O operation, the associated callback or promise is pushed onto the event queue, waiting to be executed by the event loop. This allows Node.js to handle multiple operations without being blocked by any single operation.
Example
Here’s a complete example of a simple Node.js server that demonstrates these concepts:
// server.js
const http = require('http');
// Function to simulate a delay (e.g., database query)
function delayResponse(seconds, callback) {
setTimeout(() => {
callback(`Response after ${seconds} seconds`);
}, seconds * 1000);
}
// Create an HTTP server
const server = http.createServer((req, res) => {
if (req.url === '/') {
// Simulating a non-blocking I/O operation
delayResponse(2, (responseMessage) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(responseMessage);
});
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
}
});
// Start the server
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
});
Running the Example
-
Save the Code: Save the above code in a file named
server.js
. -
Run the Server: Open a terminal and run the server using Node.js:
node server.js
-
Make Requests: Open your browser or use a tool like
curl
to make requests:curl http://localhost:3000/
You should see the response after a delay of 2 seconds:
Response after 2 seconds
-
Simultaneous Requests: Open multiple browser tabs or use multiple
curl
commands to see how Node.js can handle concurrent requests. Each request will wait for its own response without blocking others.
Concurrency Through Callbacks
Handling Requests
- When a request comes in, Node.js can initiate multiple asynchronous operations simultaneously. This means it can start processing other requests while waiting for I/O operations to complete.
Event-Driven Architecture
- The architecture enables Node.js to manage numerous connections efficiently, making it suitable for applications that require high concurrency, such as web servers and real-time applications.
Conclusion
In summary, Node.js is referred to as a single-threaded environment due to its use of a single main thread to execute JavaScript code. However, its non-blocking I/O model and event-driven architecture, managed by the event loop, enable it to handle millions of concurrent requests efficiently. This design makes Node.js a powerful choice for building scalable web applications and services. The provided example illustrates how asynchronous operations allow Node.js to manage multiple requests without blocking.