node:os answers questions about the host: how many cores, how much memory, which platform. Most of it wraps libuv, so the same call returns something sensible on Linux, macOS and Windows.
import os from 'node:os'; const gib = (n) => (n / 2 ** 30).toFixed(1);
const cpu = os.cpus(), par = os.availableParallelism();
console.log(`${os.type()} ${os.release()} (${os.platform()}/${os.arch()}) — ${os.version()}`);
console.log(`cpu : ${cpu[0].model.trim()}`);
console.log(`cores : ${cpu.length} logical, availableParallelism ${par}`);
console.log(`memory : ${gib(os.totalmem())} GiB total, ${gib(os.freemem())} GiB free`);
console.log(`loadavg : [${os.loadavg()}] uptime ${(os.uptime() / 3600).toFixed(1)} h`);Windows_NT 10.0.26200 (win32/x64) — Windows 11 Home cpu : Intel(R) Core(TM) i9-7980XE CPU @ 2.60GHz cores : 36 logical, availableParallelism 36 memory : 127.7 GiB total, 90.5 GiB free loadavg : [0,0,0] uptime 183.8 h
Size your pools with os.availableParallelism(), never os.cpus().length. The two agree on a bare machine — 36 here — but availableParallelism() wraps uv_available_parallelism(), which honors CPU affinity masks and cgroup quotas: a container limited to two CPUs on a 64-core host still sees 64 from cpus().length, and a pool sized from that number spends its life context-switching.
os.loadavg() returns [0, 0, 0] on Windows, which has no load average, and os.freemem() is the host's free memory rather than your container's limit — inside a cgroup use process.constrainedMemory().