In the previous part of Under the Hood, we looked at how HTTP requests work.
But once an HTTP request reaches our server, another question appears:
What actually happens inside Node.js when a request arrives?
If multiple users send requests at the same time, does Node.js create a new thread for every request?
If one request is waiting for a database response, does the entire server stop?
The answer lies in how Node.js handles JavaScript execution, asynchronous operations, the event loop, and I/O.
Let's go under the hood.
1. What is Node.js?
Node.js is a JavaScript runtime that allows us to execute JavaScript outside the browser.
Normally, JavaScript runs inside environments such as a browser.
With Node.js, we can run JavaScript on a server and build:
- Web servers
- REST APIs
- Backend applications
- Real-time applications
- Command-line tools
- Network services
Node.js uses the V8 JavaScript engine to execute JavaScript.
V8 is the same JavaScript engine originally developed for Google Chrome.
But Node.js is more than just V8.
It also provides APIs for things such as:
- File systems
- Networking
- Timers
- HTTP
- Streams
- Processes
A simplified view looks like this:
Your JavaScript Code
↓
Node.js
↓
V8
↓
Machine / OS
But this doesn't explain how Node.js handles many requests efficiently.
For that, we need to understand its execution model.
2. What Happens When a Request Arrives?
Suppose we create a simple Node.js server:
const http = require("http");
const server = http.createServer((req, res) => {
res.end("Hello World");
});
server.listen(3000);
When a client requests:
GET / HTTP/1.1
Node.js receives the request and executes the callback:
(req, res) => {
res.end("Hello World");
}
The simplified flow is:
Client
↓
HTTP Request
↓
Node.js
↓
Request Handler
↓
Response
↓
Client
But there is an important question:
What happens when another request arrives while the first request is being processed?
This is where the event loop becomes important.
3. Is Node.js Single-Threaded?
You will often hear:
"Node.js is single-threaded."
This statement is useful for beginners, but it is an oversimplification.
The JavaScript code that we write normally runs on a single main thread.
That means Node.js does not normally create a new JavaScript thread for every incoming request.
Instead, Node.js uses an event-driven, non-blocking I/O model.
This allows the main JavaScript thread to keep handling other work while certain operations are being handled asynchronously.
A simplified picture:
Node.js
|
JavaScript Thread
|
Event Loop
|
-------------------
| | |
Network File I/O Timers
This is one of the most important ideas behind Node.js.
4. What is the Event Loop?
The event loop is a mechanism that allows Node.js to handle asynchronous operations without blocking the main JavaScript thread.
Consider:
console.log("Start");
setTimeout(() => {
console.log("Timer finished");
}, 1000);
console.log("End");
The output is:
Start
End
Timer finished
Why?
Because Node.js doesn't stop executing JavaScript for one second.
Instead, the timer is registered, and Node.js continues executing the rest of the code.
Later, when the timer is ready, its callback can be executed.
Simplified:
Start
↓
Register Timer
↓
Continue JavaScript
↓
End
↓
Wait for Timer
↓
Execute Callback
This is the basic idea of asynchronous execution.
5. Blocking vs Non-Blocking Code
Understanding blocking and non-blocking operations is extremely important when working with Node.js.
Blocking
A blocking operation prevents the JavaScript thread from continuing with other JavaScript work.
For example:
const fs = require("fs");
const data = fs.readFileSync("data.txt");
console.log(data);
readFileSync() waits until the file has been completely read.
During that time, the JavaScript execution cannot continue past that statement.
Non-Blocking
Node.js also provides asynchronous APIs:
const fs = require("fs");
fs.readFile("data.txt", (err, data) => {
console.log(data);
});
console.log("Server is still running");
Now Node.js can continue executing other JavaScript while the file operation is being handled.
The simplified flow is:
JavaScript
↓
Start File Operation
↓
Continue Executing
↓
Other Requests
↓
File Operation Completes
↓
Callback Executes
This is why asynchronous I/O is so important for Node.js applications.
6. What is I/O?
I/O means Input/Output.
Examples include:
- Reading a file
- Writing a file
- Sending data over a network
- Receiving data from a database
- Making an external API request
These operations can involve waiting.
For example:
Node.js
↓
Database Query
↓
Waiting for Response
│
└──→ Node.js handles other work
↓
Database Response
If Node.js simply blocked the JavaScript thread every time it had to wait for I/O, handling many requests would become inefficient.
Instead, Node.js can continue working on other tasks.
7. A Real HTTP Example
Consider this server:
const http = require("http");
const server = http.createServer((req, res) => {
setTimeout(() => {
res.end("Response");
}, 2000);
});
server.listen(3000);
Suppose three users send requests:
User A → Request
User B → Request
User C → Request
Node.js doesn't necessarily need to completely finish A before it can begin handling B and C.
Conceptually:
Request A ────────┐
│
Request B ────────┼──→ Event Loop
│
Request C ────────┘
The asynchronous work can complete later, and the corresponding callback can be executed.
This is one reason Node.js is commonly used for applications that handle many I/O-heavy operations.
8. Where Does libuv Come In?
There is another important piece underneath Node.js:
libuv
libuv is a library used by Node.js that provides asynchronous I/O capabilities and the event-loop implementation.
A simplified architecture looks like:
JavaScript Application
↓
Node.js
↓
V8
↓
libuv
↓
Operating System
libuv helps Node.js deal with asynchronous operations such as networking and certain file-system operations.
It also provides a thread pool for some operations that cannot be handled directly through the operating system's asynchronous interfaces.
So saying:
"Node.js has one thread and therefore can only do one thing at a time"
is misleading.
The more accurate explanation is:
JavaScript execution normally happens on a single main thread, while Node.js uses asynchronous I/O mechanisms and, for some operations, a thread pool to handle work outside that main JavaScript execution path.
9. What is the Thread Pool?
libuv maintains a thread pool that can be used for certain expensive or blocking operations.
For example, some file-system operations and cryptographic operations may use the thread pool.
A simplified view:
Node.js
|
JavaScript Thread
|
Event Loop
|
----------------
| |
Async I/O Thread Pool
|
Worker Threads
The important point is:
The thread pool is not the same thing as creating one thread per HTTP request.
Node.js still executes your JavaScript callbacks on the main JavaScript thread.
10. What Happens During a Database Request?
Imagine an API:
app.get("/users", async (req, res) => {
const users = await User.find();
res.json(users);
});
When the database query is started, the JavaScript execution does not simply sit there doing nothing until the database responds.
The operation is asynchronous.
Conceptually:
HTTP Request
↓
Express Route
↓
Database Query
↓
Waiting for Database
│
└────→ Node.js can handle other work
↓
Database responds
↓
Continue execution
↓
Send HTTP Response
This is one reason async/await is so useful.
It makes asynchronous code easier to read without turning it into blocking code.
11. What Does await Actually Mean?
Consider:
const users = await User.find();
A common misconception is:
"
awaitblocks Node.js."
It doesn't block the entire Node.js process in the same way a synchronous blocking operation would.
Instead, await pauses the execution of that particular asynchronous function until the Promise settles.
Other work can continue through the event loop.
Conceptually:
Request A
↓
await Database Query
↓
Function pauses
↓
Node.js handles other work
↓
Database responds
↓
Function continues
This distinction is extremely important.
12. What Happens When You Make a CPU-Heavy Operation?
Node.js works particularly well for I/O-heavy applications.
But there is an important limitation.
Consider:
let total = 0;
for (let i = 0; i < 10000000000; i++) {
total += i;
}
This is CPU-intensive work.
Because JavaScript execution is happening on the main thread, a long-running CPU-heavy task can prevent the event loop from processing other JavaScript work efficiently.
The result can look like:
Heavy CPU Task
↓
Main Thread Busy
↓
Event Loop Delayed
↓
Other Requests Wait
So Node.js being non-blocking does not mean every type of work is automatically non-blocking.
CPU-heavy work needs different strategies, such as:
- Worker Threads
- Child Processes
- Moving computation to another service
- Queue-based processing
13. The Complete Request Journey
Let's put everything together.
Suppose a browser sends:
GET /users
The simplified journey is:
Client
|
↓
HTTP Request
|
↓
Node.js
|
↓
Event Loop
|
↓
Express Handler
|
↓
Database Query
|
↓
Async Waiting
|
┌─────────┴─────────┐
↓ ↓
Other Requests Database
| Response
| |
└─────────┬─────────┘
↓
Continue Code
|
↓
HTTP Response
|
↓
Client
This is the basic idea behind how a Node.js backend handles asynchronous requests.
14. Why Node.js Works Well for Web Applications
Node.js is particularly useful for applications that perform a lot of I/O operations.
Examples include:
- REST APIs
- Real-time applications
- Chat applications
- Streaming applications
- Web servers
- API gateways
The reason isn't simply:
"Node.js is fast."
The more useful explanation is:
Node.js can efficiently handle many I/O-bound operations by avoiding unnecessary blocking of the main JavaScript execution thread.
15. Common Misconceptions
❌ "Node.js creates one thread for every request."
Not normally.
Node.js primarily uses a single JavaScript execution thread rather than creating a dedicated JavaScript thread for every request.
❌ "Node.js can never use multiple threads."
Not true.
Node.js and its underlying components can use additional threads, including libuv's thread pool and mechanisms such as Worker Threads.
❌ "await blocks the entire server."
No.
await pauses the current asynchronous function while allowing other work to continue.
❌ "Non-blocking means nothing ever waits."
No.
Operations such as network and database requests still involve waiting for external systems.
The difference is that Node.js can use that time to handle other work.
16. The Mental Model
If you remember only one thing from this article, remember this:
Node.js
|
┌───────┴────────┐
↓ ↓
JavaScript Async I/O
Thread |
↓ ↓
Event Loop OS / APIs
| |
└───────┬────────┘
↓
Callbacks /
Promises
Node.js doesn't make waiting disappear.
It makes it possible to handle waiting without unnecessarily blocking the main JavaScript execution thread.
17. Conclusion
When a request reaches a Node.js server, there is much more happening than simply running a function.
Node.js uses a JavaScript execution thread, event loop, asynchronous I/O, and libuv to handle work efficiently. Instead of blocking the main thread while waiting for operations such as network or database requests, Node.js can continue handling other work and return to the original operation when it is ready.
The key idea to remember is:
Node.js doesn't eliminate waiting. It avoids unnecessarily blocking the main JavaScript execution thread while waiting.
However, this model also has limits. CPU-heavy JavaScript can keep the main thread busy and delay other requests, which is why techniques such as Worker Threads, Child Processes, and background processing can be useful for CPU-intensive tasks.
Understanding this request-handling model gives us a much better picture of what happens inside a Node.js backend.
And we're not done yet.
18. What's Next?
So far, we have followed the request from the browser to Node.js.
But most Node.js applications don't handle every request using raw Node.js APIs.
They use frameworks such as Express.
And Express introduces another important concept:
Middleware
When a request enters an Express application, it can pass through multiple pieces of middleware before reaching the final route handler.
So the next question is:
What exactly happens when a request passes through Express middleware?
That's what we'll explore in the next part of Under the Hood.
Top comments (0)