Is Node.js Really Single-Threaded?
Table of contents
“Node.js is single-threaded.”
If that’s true, how can one server handle thousands of users at the same time?
The Short Answer
JavaScript runs on one main thread. But Node.js uses multiple threads behind the scenes for heavy work like reading files or making network calls.
What Actually Happens
Your request enters the Event Loop, heavy work gets handed to the Thread Pool, and the result comes back when ready.
- User sends a request (read a file, call a database)
- Node.js receives it and passes it to the Event Loop
- Event Loop spots the heavy work and offloads it to the Thread Pool
- Thread Pool handles it in the background using multiple OS threads
- When done, the result returns to the Event Loop and your callback runs
Explain Like I’m 12
Think of Node.js like a restaurant:
- The waiter (Event Loop) takes orders from every table
- The kitchen (Thread Pool) does the actual cooking
- The customer (your request) doesn’t block other tables while food is being prepared
- When the food (result) is ready, the waiter delivers it
The waiter never stands around at one table waiting. That’s how one server handles thousands of requests at once.
Example
const fs = require("fs");
fs.readFile("big-file.txt", () => {
console.log("done");
});
console.log("continue");
Output:
continue
done
Why? fs.readFile is handed off to the Thread Pool immediately. Node.js doesn’t wait for it. The next line runs right away. When the file is ready, the callback fires and prints “done”.
Why People Call It Single-Threaded
| JavaScript | Node.js Runtime | |
|---|---|---|
| Threads | Single | Multiple |
| Role | Runs your logic | Handles background work |
Your JavaScript always runs on one thread. The runtime uses more threads under the hood so your code never blocks.
Inside the Thread Pool
The Thread Pool is provided by libuv, a C library bundled inside Node.js. libuv actually gives Node.js two different async tools:
| Thread Pool | OS Async I/O | |
|---|---|---|
| Used for | File I/O, DNS, crypto | Network (TCP/UDP) |
| Threads | 4 by default | None needed |
Network requests skip the Thread Pool entirely. The OS handles them through its own async system, so they don’t compete for those 4 slots.
Default size: 4 threads. You can raise it with an environment variable:
UV_THREADPOOL_SIZE=8 node server.js
Uses the Thread Pool:
fs.readFile,fs.writeFile, file statsdns.lookup()- Crypto:
pbkdf2,randomBytes,bcrypt(via native addons)
Skips the Thread Pool:
http.get(),https.get()- TCP and UDP sockets
dns.resolve()
Why this matters: if 100 requests all call fs.readFile at once, only 4 run at the same time. The rest queue up and wait. Network requests don’t have this bottleneck.
Benefits
- Simple mental model: no locks, no race conditions in your JS code
- Handles many concurrent users efficiently
- Great for APIs and real-time applications
- Great for network-heavy workloads
The Limitation
If one JavaScript task takes too long, everyone waits. The main thread is stuck, and background threads can’t help.
This is a problem for:
- Video encoding
- Large calculations
- Image processing
For CPU-heavy work, Node.js is the wrong tool.
Final Takeaway
Node.js is not “only one thread”. A better description is: single-threaded JavaScript running on top of a multi-threaded runtime.