Node hands async fs, dns.lookup, async zlib and async crypto calls, and native addons such as bcrypt and sharp, to libuv's threadpool. The pool has four threads whatever the machine, and its size is read from UV_THREADPOOL_SIZE the first time it is used, so only the environment can change it. Nub sets the variable when it starts Node.

nub server.ts                        # sized to the cores: 16 on 16 vCPUs
node server.ts                       # pool of 4
UV_THREADPOOL_SIZE=6 nub server.ts   # your value, used as is

The size is the larger of four and the cores available to the process, so a container gets its cgroup quota rather than the host's core count. A container's thread limit caps it. Windows stops at eight.

Read the full docs for the threadpool on docs.libuv.org.

Priority

On Linux the threads beyond the first four run at nice 10, about a tenth of the scheduler weight of a normal thread. On an idle box they run at full speed. Under contention they yield most of the CPU to the other threads in the same cgroup, which includes your own event loop.

A nice value ranks a thread only against the tasks in its own cgroup. What keeps the pool from taking CPU from a neighbouring container or service is that cgroup's own CPU share, which the kernel fixes by weight however many threads the group runs. So a larger pool does not take CPU from another container, and inside one cgroup the demotion is what keeps it off everything else there.

Child processes

A process your app spawns, including a cluster worker, starts with Node's default of four. A child that runs through nub is sized again. A UV_THREADPOOL_SIZE you set, in the shell or in an env file, is inherited as under Node.

Opting out

A UV_THREADPOOL_SIZE of your own wins. --node and NODE_COMPAT leave the pool at Node's default.

The pool size is also how many tasks run at once, so a memory-heavy task such as resizing a large image holds more in flight on a bigger pool. Set the variable yourself when memory is the limit rather than CPU.