Is Node.js Single-Threaded… or Not?

You’ve probably heard:
“Node.js is single-threaded.”
That statement is only partially correct.
The JavaScript engine (V8) is single-threaded.
Node.js as a runtime is not.
Under the hood, Node.js uses multiple threads — through libuv and the operating system — to handle I/O and computationally expensive work.
So the real question isn’t whether Node.js is single-threaded.
It’s:
Which part of Node.js are we talking about?
Let’s break this down using two concrete examples and a precise mental model of what actually happens inside the runtime.
First: What Does “Single-Threaded” Even Mean?
When we say something is single-threaded, we usually mean:
- There is one main call stack.
- Only one piece of JavaScript executes at a time on that thread.
- The main execution context does not run multiple JavaScript functions in parallel.
In Node.js, that part is true — by default.
The JavaScript execution environment (V8) contains:
V8
├─ Call Stack
├─ Heap
└─ Microtask Queue
There is exactly one main call stack responsible for executing JavaScript.
However, this does not mean JavaScript can never run in parallel. Node.js allows parallel execution when you explicitly use mechanisms like Worker Threads or separate processes. But unless you opt into those models, JavaScript runs on a single main thread.
And Node.js is more than V8.
Underneath, it includes:
- Native bindings (C++)
- libuv
- An event loop
- A thread pool
And that’s where the nuance begins.
Example 1 — setTimeout
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
console.log("End");
What Happens Internally?
Step 1 — console.log("Start")

Executes immediately inside V8.
Call stack:
[ console.log("Start") ]
Output:
Start
Pure synchronous execution on the main thread.
Step 2 — setTimeout(...)

V8 encounters setTimeout and delegates the timer registration:
V8
↓
Node.js bindings (C++)
↓
libuv
libuv:
- Registers the timer in its internal timer data structure.
- Stores the callback.
- Returns control immediately to V8.
No callback has executed yet.
Step 3 — console.log("End")

The main thread continues uninterrupted.
Output:
Start
End
Step 4 — Event Loop (Timers Phase)

The event loop continuously iterates through its phases.
When it reaches the timers phase, and the main call stack is empty:
- Expired timers are processed.
- The callback is moved to the execution queue.
- V8 executes it on the main call stack.
Final output:
Start
End
Timeout
Is this single-threaded?
Yes — JavaScript execution remained single-threaded.
No two JavaScript functions executed in parallel on the main thread.
However, timer management was handled by libuv, outside the main call stack.
That distinction is crucial.
Example 2 — CPU-Heavy Work (crypto)
const crypto = require("crypto");
console.log("Start");
crypto.pbkdf2("a", "b", 100000, 64, "sha512", () => {
console.log("Hash done");
});
console.log("End");
This is where the distinction becomes clear.
pbkdf2 is computationally expensive.
If the main JavaScript thread had to perform this computation itself, it would block the call stack and prevent other JavaScript from running.
But in Node.js, the asynchronous crypto.pbkdf2 API offloads this work so the main thread can keep executing.
What Happens Internally?
Step 1 — console.log("Start")

Executes immediately on the main JavaScript thread (V8).
Output:
Start
Step 2 — crypto.pbkdf2(...)

Flow (conceptually):
V8
↓
Node.js bindings
↓
libuv
↓
Thread Pool (worker thread)
Node delegates the heavy computation to libuv’s thread pool (default size is 4, configurable via UV_THREADPOOL_SIZE).
Now real parallel work happens:
- While a worker thread computes the hash, the main JavaScript thread remains free to continue running other code.
The main JavaScript thread is free.
Step 3 — console.log("End")

Executes immediately (because the CPU-heavy work is not running on the main call stack).
Output:
Start
End
Meanwhile, in the background:
Worker Thread #1 → computing hash
That is genuine multi-threaded behavior in the runtime.
Step 4 — When the Worker Finishes

Once the worker completes:
-
The result is reported back to the event loop.
-
The callback is queued to run on the main thread.
-
V8 executes the callback:
console.log("Hash done");
Final output:
Start
End
Hash done
So… Is Node.js Single-Threaded?
Here is the precise answer:
- JavaScript execution in Node.js is single-threaded by default.
- Node.js as a runtime is multi-threaded.
- libuv uses worker threads and operating system–level asynchronous I/O.
- Parallel JavaScript execution is possible, but only when you explicitly opt into it (for example, using Worker Threads or separate processes).
- JavaScript does not run multiple functions in parallel on the main thread.
- But Node.js absolutely uses multiple threads behind the scenes to enable concurrency and performance.
The confusion usually comes from mixing these two layers: execution and orchestration.
A More Useful Mental Model
Instead of memorizing whether Node.js is “single-threaded” or “multi-threaded,” it’s more helpful to think in terms of boundaries.
You can visualize Node.js like this:

This diagram is not the conclusion — it’s a framework.
When reasoning about threads in Node.js, ask yourself:
- Where is this code executing?
- Is it running on the main call stack?
- Is it being delegated to libuv?
- Is the operating system handling it asynchronously?
- Or have I explicitly introduced parallelism?
Understanding Node.js isn’t about labels.
It’s about understanding where execution lives, where delegation happens, and where parallelism is introduced.
That mental model scales far beyond this example.
If You Want to Go Deeper Into the Event Loop
This article simplified the event loop to focus on the threading model.
If you want a full breakdown of the event loop phases — including:
- timers
- pending callbacks
- poll
- check
- close callbacks
- and process.nextTick
You can read the official Node.js documentation here:
https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick
That page explains the exact order of phases and how they interact with callbacks and microtasks.
Final Thoughts
Node.js is multi-threaded by design.
But JavaScript execution inside Node.js remains single-threaded unless you explicitly opt into parallelism.
Multiple threads exist —
they just aren’t always used for JavaScript execution.
Understanding that distinction is what separates surface-level knowledge from real runtime understanding.