# More threads on an already busy machine

The bcrypt numbers in [Nub’s thread-pool PR](https://github.com/nubjs/nub/pull/919) make a good case for increasing the pool on the machines tested. The part I disagree with is choosing that setting for every application from its CPU count. Before starting more work, we need to know what else is using the machine.

* * *

## **“available” does not mean free**

Suppose a process reports sixteen through `os.availableParallelism()`. Those CPUs could already be busy running other applications. The function estimates how much parallelism the process can use under the OS constraints; it doesn’t check whether the CPUs are idle. Nub uses Rust’s `available_parallelism`, with the same limitation.

Now let several processes make that sizing decision. Each sees sixteen, each starts its workers, and all of them compete for the same CPU time. There’s no reservation attached to the number sixteen.

For bcrypt, this matters immediately. On a machine with no spare CPU capacity, twelve more compression or hashing jobs have to take time from something already running. That includes the event loop. Jobs can take longer to finish, and context switches and cache contention can reduce overall throughput. The closing comment on [Node.js PR #61533](https://github.com/nodejs/node/pull/61533#issuecomment-5244734518) gives this as the objection to automatic sizing.

* * *

## **Nice is slow**

Nub lowers the priority of Linux workers beyond the first four to nice 10. Its busy-host measurements show much less interference with neighbouring processes after that change. That’s useful.

But nice 10 isn’t an instruction to wait for an idle CPU. The scheduler still gives those workers a share of CPU time when the host is busy. Their jobs also retain memory and compete for caches and memory bandwidth.

There’s a cost on the request side, too. Say a bcrypt job has started on one of the lower-priority workers. If that worker gets less CPU time, the request may wait longer. Freeing another worker won’t move the running job over to it. Protecting the neighbours can therefore come at the expense of completion latency for those requests.

* * *

## **One vCPU, four workers**

There’s an important correction for small containers: Nub doesn’t set the pool size to ten. Ten is the Linux nice value for workers added beyond the original four. If Nub correctly detects one vCPU or less, it leaves you with four workers at their original priority, just as Node does. That can still be too much concurrent hashing for the container’s CPU allowance.

The [one-CPU gzip test](https://github.com/platformatic/gzip-eval-async) gives a concrete example. For 2 MiB inputs with an 8:1 compression target, one async worker and one outstanding job delivered 49.3 MiB/s. Four workers and four outstanding jobs delivered 37.3 MiB/s. Both worker count and admitted concurrency changed. These were short, synthetic Docker Desktop trials, but adding concurrency clearly didn’t help that case.

* * *

## **Waiting for storage is different**

A filesystem worker waiting for storage uses little CPU. Starting another I/O request during that wait can be worthwhile, even if there are more workers than CPUs. Bcrypt spends its time computing, so it can’t offer the same opportunity.

A read served from cache is a different case again: copying the data takes CPU time and memory bandwidth. And sending more requests to storage won’t help once the device is at capacity. In the Node.js PR, the larger pool improved the crypto benchmark but made one filesystem benchmark slower. I wouldn’t choose a default for both from either result alone.

* * *

## **Conclusion**

My objection is to making this automatic. I’d happily increase the pool for an application whose measurements justify it, but I’d test on a busy host and look at slow requests as well as throughput. A nice value doesn’t answer that question for us. And we still have to stop accepting work when the queue’s job or byte budget is full; adding threads can’t make an unbounded queue safe.
