Last updated: Aug 4, 2026

Node.js is not Single-Threaded (And Never Was)

One of the most repeated statements in backend development is:

"Node.js is single-threaded."

Almost every developer has heard it.

Many tutorials teach it.

Many interviews expect it.

And technically, it isn't entirely wrong.

But it isn't entirely correct either.

Because if Node.js were truly single-threaded, how could it:

  • ✦handle thousands of active network connections simultaneously,
  • ✦read files asynchronously,
  • ✦perform database queries without blocking the application,
  • ✦compress and decompress data in the background,
  • ✦hash passwords using CPU-intensive algorithms,
  • ✦and continue serving incoming requests while all of that is happening?

Something doesn't add up.

Imagine a simple Express server.

One user uploads a large file.

Another requests some data from a database.

A third triggers password verification during login.

Meanwhile, hundreds of other users continue sending requests.

If everything were happening on a single thread, wouldn't the entire application be forced to wait for each task to finish before moving to the next one?

Clearly, that isn't what happens.

The reality is that most developers accidentally mix up two different things: JavaScript execution and Node.js itself.

JavaScript execution inside Node.js runs on a single main thread. Node.js does not.

Under the hood, Node.js uses multiple threads, operating system facilities, background workers, thread pools, and even allows multiple JavaScript execution threads when needed.

In other words, the statement:

"Node.js is single-threaded"

is one of the most widely repeated oversimplifications in modern backend development.

To understand why, we need to look at what actually happens after a line of JavaScript leaves your code and enters the Node.js runtime.

What People Actually Mean

Before we start arguing, we should first understand why people say it in the first place. Because there is a piece of truth hidden inside it.

When developers say Node.js is single-threaded, they're usually talking about JavaScript execution. Inside a Node.js process, your JavaScript code runs on a single main execution thread. That means only one piece of JavaScript can execute at a time.

For example:

JS
while (true) {}

This infinite loop occupies the JavaScript thread forever.

As a result:

  • ✦Timers stop executing.
  • ✦Incoming requests stop being processed.
  • ✦Callbacks never run.
  • ✦The event loop becomes blocked.

Everything appears frozen because the JavaScript thread never gets a chance to do anything else. This behavior is exactly what we'd expect from a single-threaded execution model.

Only one JavaScript task can run at any given moment, and a long-running operation can prevent everything else from making progress.

So if we're talking strictly about JavaScript execution, then yes:

JavaScript inside Node.js is single-threaded.

And that's where most explanations stop.

The problem is that many developers then make a second assumption:

JavaScript is single-threaded, therefore Node.js must also be single-threaded.

That's the part where the mental model starts breaking down.

Then Who Is Doing The Work?

Let's go back to a simple example:

JS
import fs from "node:fs";

fs.readFile("huge-file.txt", () => {
  console.log("done");
});

Most developers have written code like this hundreds of times.

The file gets read.

The callback executes.

Everything works exactly as expected.

But if JavaScript execution is running on a single thread, a more interesting question appears:

Who is actually reading the file?

Because it certainly isn't the JavaScript thread. Think about it. The moment fs.readFile() is called, JavaScript continues executing. The application doesn't freeze. The process can still:

  • ✦handle incoming requests,
  • ✦execute timers,
  • ✦process other callbacks,
  • ✦and continue running additional JavaScript code.

Meanwhile, somewhere in the background, a potentially large file is being read from disk.

So where is that work happening?

And file operations aren't the only example. What about crypto.pbkdf2(...) for password hashing? Or zlib.gzip(...) for compression?

Or a server handling thousands of active network connections simultaneously?

If JavaScript can only execute one thing at a time, then something else must be doing the actual work.

Something outside the JavaScript thread.

And this is exactly where the common explanation starts falling apart. Because while your JavaScript runs on a single thread, Node.js is doing far more behind the scenes than most developers realize.

The Mental Model Most Developers Have

Many developers imagine Node.js working like this:

JavaScriptMain Thread
Event LoopAsync Nature
Everything HappensHidden Work

It sounds reasonable.

JavaScript runs.

The Event Loop manages async work.

Requests get handled. Files get read. Passwords get hashed. The application keeps running.

The problem is that this model quietly hides most of the runtime.

It doesn't explain who reads the file.

It doesn't explain who performs the hashing.

It doesn't explain how thousands of sockets remain active simultaneously.

And it definitely doesn't explain where all that work is actually happening. So before we say: "Node.js is single-threaded"

we need to answer a more important question:

Which part of Node.js are we talking about?

Node.js Is More Than V8

To answer that question, we need to zoom out. Because Node.js is not just a JavaScript engine. That's actually one of the biggest reasons this topic becomes confusing. Most developers mentally equate Node.js with JavaScript execution. But JavaScript is only one piece of the runtime.

At its core, Node.js is really the combination of three major pieces:

  • ✦V8
  • ✦libuv
  • ✦Native C++ bindings connecting them

If you've only ever thought about Node.js as a JavaScript runtime, you've really only been thinking about the first piece.

V8

V8 is Google's JavaScript engine, written in C++. Its job is simple:

Take JavaScript code and execute it.

This is where your application's JavaScript runs. This is also where the famous "single-threaded" behavior comes from. Your JavaScript code executes on a single main execution thread inside V8.

But V8 alone cannot explain how Node.js performs asynchronous file operations, handles network connections, or runs CPU-intensive tasks in the background. Something else is involved.

libuv

This is where libuv enters the picture. libuv is a cross-platform C library responsible for much of Node.js's asynchronous behavior. Whenever your application performs operations like:

  • ✦reading files,
  • ✦writing files,
  • ✦handling network connections,
  • ✦performing DNS lookups,
  • ✦compressing data,
  • ✦or running cryptographic functions,

libuv is usually involved somewhere behind the scenes. In many cases, it is libuv—not the JavaScript thread—that is responsible for coordinating and executing the actual work.

The Glue Between Them

Of course, V8 and libuv cannot communicate directly by magic. Node.js contains a large collection of native C++ bindings that sit between them. These bindings expose JavaScript APIs such as:

  • ✦fs.readFile(...)
  • ✦http.createServer(...)
  • ✦crypto.pbkdf2(...)

while internally forwarding the work to the appropriate native systems.

So when you write:

JS
fs.readFile("huge-file.txt", callback);

the JavaScript thread isn't actually opening the file and reading bytes from disk itself. Instead, that request leaves V8 and enters the rest of the Node.js runtime. And that's where things start becoming much more interesting.

JavaScript code(Your Appliction)
V8JavaScript Engine
Node.js C++ BindingsNative APIs
libuvAsync I/O Layer
OS Async I/Oepoll/kqueue/IOCP
Thread Poolfs, crypto, zlib, DNS

But Node.js itself is much larger than V8. And once we start looking at what V8 is actually surrounded by, the single-threaded myth begins to fall apart very quickly.

The Two Async Paths

At this point, we know that when JavaScript calls something like:

JS
fs.readFile("huge-file.txt", callback);

the actual work leaves V8 and enters libuv. The next question is:

What happens after that?

The answer is that libuv doesn't handle every operation the same way. Depending on the type of task, libuv chooses one of two completely different paths.

Path A: Operating System Async I/O

For many networking operations, libuv doesn't create worker threads at all. Instead, it asks the operating system to watch the connection and notify it whenever something interesting happens.

For example:

  • ✦incoming HTTP requests,
  • ✦TCP connections,
  • ✦sockets,
  • ✦pipes,
  • ✦outgoing network requests.

Modern operating systems already have extremely efficient mechanisms for monitoring thousands of connections simultaneously. Depending on the operating system, this might be:

  • ✦epoll on Linux,
  • ✦kqueue on macOS,
  • ✦IOCP on Windows.

The exact implementation isn't important. The important part is this:

The operating system does the waiting.

Which is a pretty important detail. Because most people assume Node.js is doing the waiting. It isn't.

Node.js does not create a thread for every connection. It simply registers interest in an event and continues executing other work. When data becomes available, the operating system notifies libuv, and libuv schedules the corresponding JavaScript callback to run later.

This is one of the reasons a single Node.js process can handle thousands of active network connections simultaneously without creating thousands of threads.

Path B: The libuv Thread Pool

Unfortunately, not every operation can be handled efficiently by the operating system's asynchronous networking facilities.

Consider operations like:

  • ✦file system access,
  • ✦cryptographic functions,
  • ✦compression,
  • ✦DNS lookups.

Examples include:

  • ✦fs.readFile(...)
  • ✦crypto.pbkdf2(...)
  • ✦zlib.gzip(...)
  • ✦dns.lookup(...)

These operations need actual work to be performed. Somebody has to read the file. Somebody has to hash the password. Somebody has to compress the data. The operating system can't simply "wait" for these tasks the way it can for a network socket. So libuv takes a different approach.

It creates a *worker thread pool.

By default, this pool contains 4 worker threads. When one of these operations is requested:


1. JavaScript calls a Node.js API.

2. The request reaches libuv.

3. libuv places the task into the thread pool queue.

4. A worker thread picks up the task.

5. The JavaScript thread immediately continues executing other work.


Once the task finishes, libuv places the callback into the event loop, and eventually V8 executes that callback on the main JavaScript thread.

Wait.

If Node.js has a pool of worker threads sitting in the background...

reading files, hashing passwords, compressing data, and resolving DNS lookups...

then how exactly is Node.js single-threaded?

This is where the famous explanation starts breaking down. The JavaScript thread may be single-threaded. But the runtime clearly isn't.

The Event Loop Is Not Doing The Work

This is where another misconception sneaks in.

At this point, we have enough information to correct one of the biggest misconceptions in the Node.js ecosystem.

Most developers think the Event Loop is doing the work.

But the Event Loop never reads the file.

It never hashes the password.

It never compresses the data.

It does not wait for network packets.

Many developers imagine asynchronous operations working like this:

JavaScriptMain Thread
Event LoopCoordinator
Performs Async WorkThe Work
Callback ExecutesV8 Execution

But that isn't what actually happens. The actual work is happening somewhere else.

Either:

  • ✦the operating system is handling it,
  • ✦or a libuv worker thread is handling it.

The Event Loop simply coordinates what happens after the work is finished. A more accurate mental model looks like this:

Async Operation
OS / Thread Pool
Task Completes
Event Loop Notices
Callback Executes

Let's revisit our file reading example:

JS
fs.readFile("huge-file.txt", () => {
  console.log("done");
});

When this code runs:


1. JavaScript calls fs.readFile().

2. The request is forwarded to libuv.

3. A worker thread starts reading the file.

4. The JavaScript thread immediately continues executing other code.

5. Once the file has been read, libuv marks the task as completed.

6. The Event Loop notices that a callback is waiting.

7. V8 finally executes the callback on the JavaScript thread.


The Event Loop didn't read the file. It simply noticed that the file had already been read. This distinction is incredibly important. The Event Loop is more like a coordinator than a worker. Its job is to continuously check:

"Has any asynchronous work finished?"

If the answer is yes, it schedules the corresponding callback to run on the JavaScript thread. That's it. This is exactly why Node.js can remain responsive while multiple asynchronous operations are running simultaneously. The actual work happens elsewhere. The Event Loop simply delivers the result back to JavaScript when that work is complete. Which means the Event Loop is much closer to a coordinator than a worker. That's a subtle distinction. But it's one of the most important ideas in Node.js.

You can visualize the flow like this:

JavaScript
Node.js API
libuv
libuv
OS Async I/O
Thread Pool
Task Completes
Event Loop
Callback
JavaScript

Once you understand this flow, the phrase:

"Node.js uses an event loop to perform asynchronous work"

starts sounding a little strange. Because the Event Loop isn't performing the work at all. It's managing the completion of work that has already been performed somewhere else.

Then Why Does CPU-Heavy Code Block

At this point, something feels contradictory.

We've spent the last several pages talking about worker threads, operating system I/O, and background work.

So why can one bad loop still freeze an application?

At this point, it might sound like we've completely disproven the idea that Node.js is single-threaded. And in many ways, we have. We've already seen that:

  • ✦the operating system handles asynchronous network I/O,
  • ✦libuv uses worker threads,
  • ✦file operations happen outside the JavaScript thread,
  • ✦cryptography can run on background threads,
  • ✦compression can run on background threads.

So a natural question appears:

If Node.js uses all these threads, why does CPU-heavy JavaScript still block the application?

Consider this code:

JS
for (;;) {}

Run it inside a Node.js application and everything stops.

  • ✦Requests stop.
  • ✦Timers stop.
  • ✦Callbacks stop.
  • ✦The Event Loop stops making progress.

The entire application appears frozen.

Why?

Here's the catch.

None of those worker threads are running your application code. They're running native work delegated by libuv. Your JavaScript still runs on one main thread. And only one piece of JavaScript can execute at a time. So when you write:

JS
while (true) {
  /* expensive work */
}

/* or */

for (let i = 0; i < 10_000_000; i++){
  /* CPU-intensive calculation */
}

there is nothing to offload. No network socket to monitor. No file to read. No operating system event to wait for. No libuv worker thread automatically stepping in to help. The JavaScript thread itself must perform the work. And while it's busy doing that, it cannot process anything else. This is the distinction that most explanations miss:

JavaScript Execution = Single-Threaded
Node.js Runtime ≠ Single-Threaded

Or put another way: Node.js can use multiple threads to perform work. But your JavaScript code still executes on one main thread. That's why asynchronous file operations don't block the application.

And it's also why CPU-heavy JavaScript often does.

Worker Threads Changed Things Further

Everything we've discussed so far has existed in Node.js for years. The operating system handles asynchronous network I/O. libuv uses worker threads for file operations, cryptography, compression, and DNS lookups. The Node.js runtime was already using multiple threads behind the scenes. But modern Node.js took things even further. It introduced Worker Threads. For the first time, JavaScript itself could run across multiple threads within a single Node.js process.

Creating a worker is straightforward:

JS
import { Worker } from "node:worker_threads";

new Worker("./worker.js");

At first glance, this might look similar to a browser Web Worker. But under the hood, something much more significant is happening. When Node.js creates a Worker Thread, it doesn't simply create another execution context inside the existing JavaScript thread. Instead, it creates an entirely new V8 isolate.

That worker receives:

  • ✦its own V8 isolate,
  • ✦its own heap,
  • ✦its own event loop,
  • ✦and its own main JavaScript thread.

You can think of it as a miniature Node.js runtime living inside the same process. A simplified view looks like this:

MD
Node.js Process
│
├── Main Thread
│   ├── V8 Isolate
│   ├── Heap
│   └── Event Loop
│
├── Worker Thread
│   ├── V8 Isolate
│   ├── Heap
│   └── Event Loop
│
└── Worker Thread
    ├── V8 Isolate
    ├── Heap
    └── Event Loop

This means multiple pieces of JavaScript can now execute simultaneously on different CPU cores.

Imagine generating a large PDF.

Or processing a high-resolution image upload.

Or running a machine-learning inference.

These are workloads where your own JavaScript becomes the expensive part.

Running them on the main thread can block requests, timers, callbacks, and every other piece of application logic.

Worker Threads allow that computation to run independently while the main thread continues handling the rest of the application.

That's the real reason they exist.

A CPU-intensive calculation running inside one worker no longer has to block the main JavaScript thread. That's a very different world from the traditional "Node.js is single-threaded" explanation.

In fact, a single Node.js process can now contain multiple JavaScript execution threads running concurrently. Which completely breaks the old mental model that:

One Node.js Process = One Thread

Because that statement simply isn't true anymore. And in many ways, it never really was.

Once this model clicks, a lot of common Node.js advice starts sounding a little different.

Common Node.js Myths

Myth: Node.js Creates One Thread Per Request

Reality

Usually, it doesn't.

For networking operations, Node.js typically asks the operating system to monitor connections and notify it when something happens.

A single process can manage thousands of active connections without creating thousands of threads.

The operating system does most of the waiting.

Myth: The Event Loop Performs Asynchronous Work

Reality

The Event Loop does not perform the work.

The actual work is usually handled by:

  • ✦the operating system,
  • ✦or libuv worker threads.

The Event Loop simply notices when a task has finished and schedules the corresponding callback to run on the JavaScript thread.

It coordinates completed work.

It does not perform it.

Myth: Async Means Parallel

Reality

Not necessarily.

Asynchronous programming is primarily about concurrency, not parallelism.

A task can be asynchronous simply because JavaScript doesn't wait for it to finish.

That doesn't automatically mean multiple pieces of JavaScript are executing simultaneously.

Concurrency is about managing multiple tasks efficiently.

Parallelism is about multiple tasks actually running at the same time.

They are related concepts, but they are not the same thing.

Myth: Node.js Can't Use Multiple Threads

Reality

Node.js has been using multiple threads for a long time.

libuv already uses worker threads for:

  • ✦file system operations,
  • ✦cryptography,
  • ✦compression,
  • ✦and DNS lookups.

Modern Node.js goes even further with Worker Threads, allowing JavaScript itself to run on multiple threads when needed.



So, is Node.js single-threaded?

The honest answer is:

it depends on which layer you're looking at.

If you're talking about JavaScript execution on the main thread, then yes.

Only one piece of JavaScript executes at a time.

But if you're talking about the Node.js runtime as a whole, the picture is very different.

Behind the scenes you'll find:

  • ✦operating system async I/O
  • ✦libuv worker threads
  • ✦background runtime threads
  • ✦Worker Threads

all contributing to the work your application performs.

That's why a Node.js server can keep handling requests while files are being read, passwords are being hashed, and network connections are waiting for data.

The JavaScript thread isn't doing all of that work.

It's coordinating the results.

And that's the key idea.

The phrase: "Node.js is single-threaded" survives because it's simple.

The reality survives because it's true.

Node.js was never just one thread.

We were simply looking at the wrong one.

Module: Connection.

Initiate Connection

Open to building scalable systems and real-time applications. If you have something interesting, let’s connect.

System: Available
Connection Interface>_
Your information is secure and will never be shared.