Is Node.js Single-Threaded or Multi-Threaded?
I thought it was a stupid thing to ask, but when I really thought about it, I realised I did not even know how Node works
It started when my friend asked: “Is Node.js single-threaded or multi-threaded?”
My immediate internal reaction was, “That’s a trick question, Node isn’t a language, it’s a runtime environment. Obviously, it’s multi-threaded because it handles thousands of requests using worker threads or something..?”
But I froze as I tried to explain. I did not actually know what those threads are and how they work. If it’s multi-threaded, why do people say “Don’t block the main thread”? I realized I had a high-level idea of how it worked, but the architectural details were blurry.
And thus I went down a rabbit hole of C, C++, WebAssembly, and the Event Loop to find the truth. Here is the chronological path of my investigation through questions that popped up in my mind throughout my research:
Q1: Let's start from what we do know C is multithreaded, but how does multithreading work in C?
In a C file, when you create multiple threads, they exist in the same Process.
- The Shared Sandbox: They share the same memory (The Heap) and global variables. Thread A can write to a variable, and Thread B can read it instantly.
- The Danger: This is “True” Multithreading (Parallelism). It’s incredibly fast but requires “Mutexes” (locks) to prevent Race Conditions where two threads try to tinker with the same data at once.
Takeaway: C is multi-threaded by design. It gives you raw power and raw responsibility.
Q2: And JavaScript is single-threaded, right?
Yes, strictly speaking.
JavaScript has one Call Stack. If you write a heavy for loop, the stack is full. It cannot click a button, it cannot render a frame, and it cannot receive a network request. The Main Thread is effectively "frozen."
Q3: But what about async/await? Doesn't that make it multi-threaded?
This was my first big misconception; I did not have a clear idea about what async/await even does. I thought async meant "run this on a background thread."
async/await is about waiting, not Parallel Working. When we await a database call, Node does not spin up a new CPU thread to do the math. It tells the Main Thread: "Pause this function and go handle other events (like button clicks) while the System/libuv handles the I/O." If you put a massive math calculation inside an async function, it will still block your UI once it is handed to the main JS thread by the event loop.
Q4: So if the Main Thread is just “waiting,” who is doing the actual work?
This is where Lydia Hallie’s incredible visualization of the Event Loop helped me the most.
Node and The Event Loop:
When you do something asynchronous (like a database read), Node hands the task to the System (the OS or libuv).
I’ll try to use the famous “Fast Food Restaurant” analogy:
Picture a busy fast-food joint with one Head Chef (your Node.js main thread / V8) and a huge kitchen in the back (libuv + OS).
- The Event Loop is the calm Restaurant Manager standing behind the chef. Its only job: keep the Head Chef busy with the next task the moment the chef’s hands are free — never letting the chef wait or block.
Here’s exactly how it flows:
- Customer places an order → Head Chef starts cooking it right away (synchronous JS on the call stack).
- Order needs time (DB query, file read, API call)? Chef quickly writes the details on a ticket and slides it to the kitchen window. The kitchen starts working in the background. Chef is instantly free again.
- The moment the chef finishes the current order (call stack empties), the Event Loop Manager steps in and says:
- “First, check VIP counter” → Microtask Queue (Promises, .then(), await, process.nextTick). These get served immediately, one after another, until the VIP lane is empty.
- “Now check regular counter” → Task Queue (I/O callbacks, setTimeout, setImmediate, network data, etc.) Only one task is handed to the chef. Then the manager loops back to step 3.
Nothing ready? Manager politely waits (poll phase) without wasting CPU.
The single Head Chef never stands idle or waits for the kitchen. Thousands of orders flow through at the same time, all on one thread.
Reference: For a perfect visual of the Task Queue vs. Microtask Queue, watch Lydia Hallie’s Event Loop Visualized.
Q5: If the “Kitchen” uses libuv threads, what is the difference between those and “Worker Threads”?
After a bit of searching, I realized Node has Internal Threads (the ones it uses for you automatically) and External Threads (the ones you create yourself).
Libuv uses its Thread Pool only for operations that are inherently blocking or where no asynchronous OS interface exists.
Worker Threads are like hiring an entirely separate person to stand in a separate environment to execute a task written in JS concurrently in their own “Mini Environment”.
Get Ishan Siddhartha’s stories in your inbox
Join Medium for free to get updates from this writer.
Deep Dive: Watch Bytemonk’s Node.js Architecture and Worker Threads vs. Thread Pool to see the actual C++ structures.
Q6: What is the Mini Environment that the worker threads utilize, and does it also imply multiple libuv Thread Pools?
I found some conflicting info here, but here's what I understood:
When you spawn a Worker Thread in Node.js, it does get its own isolated “sandbox”:
- It gets its own V8 Instance.
- It gets its own Event Loop (libuv instance).
- It gets its own Custom JS Heap.
While each worker has its own “Event Loop,” they do NOT have their own thread pool. The libuv Thread Pool is GLOBAL and shared across all event loops. Whether you have 1 worker or 10, they all share the same 4 internal C++ threads (by default 4, but you can change it) for tasks like file reading or crypto. If your workers are all trying to read files at once, they will wait for each other in that shared pool.
Q7: So, should I use Worker Threads for everything?
Well, you shouldn’t :
Although Node’s native async I/O is already incredibly optimized, moving I/O to a Worker Thread actually adds overhead because you have to pass messages back and forth between threads.
Use Workers for: Heavy CPU tasks (Image processing, Big Data sorting, Encryption).
Don’t use Workers for: Database calls or Network requests.
Q8: What about WebAssembly? Where does that fit in?
If JavaScript is the “Flexible Chef” and Worker Threads are his “Extra Arms,” WebAssembly (Wasm) is the “Turbo-Charged Calculator.”
Wasm allows you to run C++ or Rust code in the browser/Node at near-native speed. It’s a binary format.
- Why would we need it even though JavaScript exists? Because JS has to “guess” types (Is this a number or a string?). Wasm is “Strongly Typed”; the CPU knows exactly what it’s doing, so it bypasses the slow parts of the JS engine.
- Example: Google Earth and Figma use Wasm to handle massive amounts of 3D data that would make standard JS crawl, so it's commonly used wherever 3d rendering is involved.
Q9: Ok, but one node process would still use a single core, right? How do they scale it in production?
Yes, exactly. The actual JavaScript logic runs on the single Main Thread, and a standard Node.js process will max out one core and leave the rest of an expensive multi-core server entirely idle.
To solve this and handle massive traffic, production environments generally rely on two scaling strategies:
1. The Single-Machine Approach (PM2) You use a process manager like PM2 (pm2 start -i max) to fork your Node application into multiple identical processes across all available CPU cores, load-balancing the traffic between them.
2. The Modern Fleet Approach (Docker & Kubernetes) Instead of packing multiple processes onto one giant server, you package a single Node process inside a Docker container. If you are building a backend API for a Next.js app or a real-time WebSocket service, you ensure the app is stateless. Then, you let Kubernetes (K8s) orchestrate spinning up dozens of these container Replicas across a fleet of servers, placing a Load Balancer in front of them to distribute incoming traffic.
The Kubernetes “Gotcha” (Time vs. Space) When you tell Kubernetes to give your Node.js Pod “1 CPU”, it does not mean “This Pod gets Core #3 permanently.” By default, Kubernetes uses the Linux Completely Fair Scheduler (CFS). It enforces a time quota. This means your Pod is allowed to use up to 100% of one core’s worth of execution time, but it shares the underlying 12-core hardware pool with everything else. The OS will rapidly migrate your Node process running on Core 1 at one moment, and Core 7 a few milliseconds later.
Here is the mental model for how Kubernetes allocates CPU:
- Default Mode (CFS Quota): “You are allowed to use 1 worker’s worth of time, but that worker can be any of the 12 employees in the office at any given moment.”
- Static Mode (CPU Pinning): If you explicitly set the K8s CPU Manager policy to “static”, it locks a specific container to a specific physical core ("Employee #5 is exclusively yours. No sharing."). This is usually reserved for ultra-low latency systems (like high-frequency trading), but for 99% of web traffic, the default time-sharing is perfect.
(Sources: Kubernetes Official Docs: CPU Management Policies & Dev Genius: How To Scale A NodeJS Application)
The Final Conclusion
So, to answer my friend’s question: “Is Node.js single-threaded or multi-threaded?”
“The JavaScript execution is single-threaded (Event Loop), but the Node.js runtime is multi-threaded (libuv). It uses a shared global thread pool for I/O, and if you need to do heavy CPU work in JavaScript, you can spawn Worker Threads with their own isolated V8 instances.”
References & Further Reading/Watching
- Lydia Hallie: JavaScript Visualized — Event Loop
- Bytemonk: Node.js Architecture & Libuv
- Bytemonk: Worker Threads Deep Dive
- Node.js Docs: Worker Threads
- libuv Docs: Thread pool work scheduling
- https://deepsource.com/blog/nodejs-worker-threads
- https://blog.devgenius.io/how-to-scale-a-nodejs-application-a51d3e8e2d36
- https://kubernetes.io/docs/tasks/administer-cluster/cpu-management-policies/





