Thread Pool
libuv hands some async work to the operating system, but some work the OS can't do asynchronously. For that, libuv keeps a small team of background threads ready. This is the Thread Pool.
Definition
The thread pool is a fixed group of background threads managed by libuv. It runs slow or blocking operations (such as file reads and password hashing) off the main thread, so your JavaScript keeps running.
Analogy: think of a bank with one front desk and 4 back-office clerks. The front desk (main thread) takes requests and passes slow paperwork to the clerks (thread pool). If 6 requests arrive, 4 clerks start at once, and the other 2 wait until a clerk is free.
| Property | Thread Pool |
|---|---|
| Managed by | libuv |
| Default size | 4 threads |
| Maximum size | 1024 |
| Changed with | The UV_THREADPOOL_SIZE environment variable |
| Runs | C/C++ work for Node.js APIs, not your JavaScript |
What Uses the Thread Pool
| Uses the thread pool | Does NOT use the thread pool |
|---|---|
File system: fs.readFile, fs.writeFile, and other async fs methods | Network I/O: http, net, TCP, sockets (handled by the OS) |
Crypto: crypto.pbkdf2, crypto.scrypt, crypto.randomBytes (async) | Timers: setTimeout, setInterval (handled by the event loop) |
Compression: zlib async methods | dns.resolve() (uses the network directly) |
DNS: dns.lookup() (also used by http.get for hostnames) | Sync methods like fs.readFileSync (run on the main thread) |
Seeing the Limit of 4 Threads
crypto.pbkdf2 is a slow hashing function that runs on the thread pool. Let's call it 6 times:
const crypto = require("crypto");const start = Date.now();for (let i = 1; i <= 6; i++) {crypto.pbkdf2("password", "salt", 100000, 64, "sha512", () => {console.log(`Hash ${i}: ${Date.now() - start} ms`);});}// Example output (times vary by machine):// Hash 1: 510 ms// Hash 3: 515 ms// Hash 2: 518 ms// Hash 4: 520 ms// Hash 5: 1020 ms// Hash 6: 1025 ms
- All 6 tasks are handed to libuv at once.
- 4 threads take the first 4 tasks, and they finish at about the same time.
- Tasks 5 and 6 wait in line until a thread is free, so they take about twice as long.
The order of the first 4 can change on each run, because they finish in parallel.
Changing the Thread Pool Size
Set UV_THREADPOOL_SIZE when you start the program:
# Linux / macOSUV_THREADPOOL_SIZE=6 node app.js# Windows (PowerShell)$env:UV_THREADPOOL_SIZE=6; node app.js
With 6 threads, all 6 hashes from the example finish at about the same time, as long as your machine has at least 6 CPU cores.
When to increase it:
- Your app does a lot of file system, crypto, or compression work at the same time.
- Many slow tasks are waiting in line for a free thread.
Be careful: more threads than CPU cores does not make CPU-heavy work faster. The threads just take turns on the same cores.
Thread Pool Starvation
All thread pool tasks share the same 4 threads. If slow tasks fill all of them, other tasks must wait, even unrelated ones.
const crypto = require("crypto");const fs = require("fs");// 4 slow hashes fill all 4 threadsfor (let i = 0; i < 4; i++) {crypto.pbkdf2("password", "salt", 500000, 64, "sha512", () => {});}// This small file read must wait for a free threadfs.readFile("small.txt", () => {console.log("File read (delayed)");});
The file is tiny, but it is delayed because all threads are busy hashing. This can also slow down http.get calls, because hostname lookups use dns.lookup.
Thread Pool vs Worker Threads
| Thread Pool | Worker Threads | |
|---|---|---|
| Created by | libuv, automatically | You, with the worker_threads module |
| Runs | Node's built-in C/C++ work (fs, crypto, zlib) | Your own JavaScript |
| Use it for | Nothing to do; Node uses it for you | CPU-heavy JavaScript, like image processing or big calculations |
| Control | Only its size | Full control |
So if your own JavaScript is heavy, the thread pool won't help. Use worker threads instead.
Summary
- The thread pool is a set of background threads managed by libuv.
- The default size is 4, changed with
UV_THREADPOOL_SIZE(maximum 1024). - It runs async
fs,crypto,zlib, anddns.lookup. - Network I/O and timers do not use it.
- With 4 threads, the 5th task waits until a thread is free.
- Slow tasks can starve the pool and delay unrelated tasks.
- The thread pool runs Node's built-in work. For your own CPU-heavy JavaScript, use worker threads.
Interview Answer: What Is the Thread Pool in Node.js?
The thread pool is a group of background threads managed by libuv. Node.js uses it for operations the operating system can't do asynchronously, such as async file system calls, crypto functions like pbkdf2, zlib compression, and dns.lookup. Network I/O does not use it, because the OS handles it asynchronously.
By default, the pool has 4 threads, and I can change it with the UV_THREADPOOL_SIZE environment variable. If more than 4 tasks are started at once, the extra tasks wait for a free thread. For example, if I run crypto.pbkdf2 6 times, the first 4 finish together, and the last 2 take about twice as long.
The thread pool only runs Node's built-in C/C++ work, not my JavaScript. For CPU-heavy JavaScript, I use worker threads.
The next topic is worker threads.