DEV Community

Aryan Gupta
Aryan Gupta

Posted on

How Node.js Handles Requests

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

When a client requests:

GET / HTTP/1.1
Enter fullscreen mode Exit fullscreen mode

Node.js receives the request and executes the callback:

(req, res) => {
    res.end("Hello World");
}
Enter fullscreen mode Exit fullscreen mode

The simplified flow is:

Client
   ↓
HTTP Request
   ↓
Node.js
   ↓
Request Handler
   ↓
Response
   ↓
Client
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

The output is:

Start
End
Timer finished
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Suppose three users send requests:

User A → Request
User B → Request
User C → Request
Enter fullscreen mode Exit fullscreen mode

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 ────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);

});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

A common misconception is:

"await blocks 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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.


References

  1. Node.js — Introduction to Node.js
  2. The Node.js Event Loop — Node.js Documentation
  3. Don't Block the Event Loop — Node.js Documentation
  4. libuv Documentation

Top comments (0)