Four unrelated subsystems, four threads, one queue
Part 5 introduced the thread pool as the thing that makes fs.readFile asynchronous, and every part since has referred to it as "the pool". That framing is true and incomplete, and the incompleteness is where the production surprises come from.
Four different subsystems share it — the filesystem, DNS name resolution, several crypto functions and all of zlib — and by default they share four threads. They have nothing to do with each other, they queue together, and none of them knows the others exist.
zlib.gzip(bigBuffer, () => {}); // ×4, one per pool thread
zlib.gzip(bigBuffer, () => {});
zlib.gzip(bigBuffer, () => {});
zlib.gzip(bigBuffer, () => {});
dns.lookup("api.example.com", () => {}); // now waits for a compression jobNothing there is misconfigured. The DNS lookup is queued behind four compression jobs because compression and name resolution are served by the same four threads, and the queue is first-in, first-out.
The pool is not "the filesystem pool". It is a general work queue for blocking C code, and every subsystem that uses it competes with every other one for the same four threads.
No readiness interface means no other option
Part 0 §8 and Part 8 established the rule that decides this. A socket can be watched: you ask the kernel to tell you when it is ready, and one thread can hold ten thousand of them. A regular file cannot — it is always ready, so the answer is useless and the only way to find out how long a read takes is to make it and see.
worker_thread, not this.Which is why the membership list looks arbitrary until you apply the test, and then looks inevitable.
fs, dns.lookup, some crypto, all zlib
Here is everything in Node that hands work to the pool, and the shape of what it submits.
readFile of a large file is one job that holds a thread for the whole read, which is why a directory walk of thousands of files saturates the pool so easily.Sync variants of all of these run on the main thread instead, which is the trap.If you have ever wondered why a Node service gets slower during a burst of gzip, and the slowdown shows up as DNS timeouts, this list is the reason. They are the same queue.
The same answer by two completely different routes
These two functions look like variants of one thing. They are not: one takes a pool thread and one does not touch the pool at all.
Almost everything resolves names through dns.lookup without asking for it: http.request, fetch, database drivers, every client library that takes a hostname. So a service making many outbound connections is submitting pool jobs constantly, and it never appears in the code as a pool operation.
dns.lookup consults the operating system — hosts file, nsswitch, everything — and that API is blocking, so it needs a thread. dns.resolve sends a DNS query over a socket, which the loop can watch like any other socket. Same question, opposite mechanism.
A compression job can delay a name lookup
Part 5 showed the pool saturating with file reads. That figure held one subsystem constant; this one is about what happens when the jobs come from different subsystems, because that is the case nobody plans for.
The queue is first-in, first-out and jobs are not preemptible. A pbkdf2 at high iteration counts holds a thread for as long as it takes, and everything behind it waits — including a DNS lookup that would have taken a millisecond.
The symptom almost never names the cause. You see slow DNS, or slow file reads, and the actual culprit is a different subsystem entirely that happens to share the queue.
Read once, at first use, and never again
The pool size is set by an environment variable, defaults to 4, and has two properties that make it easy to set ineffectively.
const fs = require("fs");
fs.readFile(__filename, () => {}); // pool created HERE, with 4 threads
process.env.UV_THREADPOOL_SIZE = 16; // no effect, everTen claims about the pool
These are the beliefs that make pool exhaustion so hard to diagnose — mostly because they scope the pool too narrowly.
Nearly every wrong take here assumes the pool belongs to one subsystem. It belongs to four, they share one queue, and the one that saturates it is rarely the one that looks slow.
If you remember nothing else
The pool is a general work queue for blocking C code, not a filesystem feature.
UV_THREADPOOL_SIZE in the environment, not after a require has already warmed the pool.That completes the picture Part 5 started. Part 5 followed one call into the pool and back; this part is everything else that was already in there with it.