NNODE LOOP LABruntime observatoryNNEON · Articles in 90 languages
CONNECTING
v24.18.0linux/x64
Hidden parallelism
05

libuv thread pool

Six PBKDF2 jobs reveal the native thread-pool queue and the callbacks returning to the Event Loop.

PROCESS IDcurrent server
UPTIMEsince startup
LOOP DELAY P95perf_hooks
UTILIZATIONevent loop
HTTP ROUNDTRIPbrowser → server
LIVE TRACE

Timeline

READY
0 ms
Events will appear hereRun the selected scenario
#TIMESOURCEEVENT
Waiting for an experiment…
CHAPTER 05
DEEP DIVE · FROM BASICS TO CODE

Understanding: libuv thread pool

You can read this chapter before running the experiment. Then return to the live trace and match each concept to a real event.

FIRST, IN PLAIN LANGUAGE

Think of a small kitchen behind the dining room. The JavaScript waiter quickly submits six orders. Only four cooks are available, so two orders wait even though the waiter is free to serve other guests.

TECHNICAL FOUNDATION

The libuv pool executes selected native operations that cannot use normal OS readiness or are computationally expensive. Application JavaScript does not execute in this pool. The pool is shared by the process, and completed native work posts a callback back to the Event Loop.

Why it mattersThe shared pool can become a hidden bottleneck: expensive crypto can increase the latency of unrelated fs or DNS work.
WHERE THE WORK RUNS
01YOUR JS CODEfunctions · callbacks
02NODE APIsfs · crypto · timers
03V8 + LIBUVheap · loop · pool
04OPERATING SYSTEMI/O · threads · memory
01 · GLOSSARY

Terms used in this experiment

Understand the words first, then the execution order.

01

Native operation

C/C++ code inside Node or a library, rather than your application JavaScript.

02

Thread pool

A fixed number of reusable threads with a queue of jobs in front of them.

03

UV_THREADPOOL_SIZE

An environment variable that sets shared pool size when the Node process starts.

04

PBKDF2

A computationally expensive key-derivation function used here to create observable pool work.

02 · MECHANICS

What happens step by step

Each step maps to an observable runtime state.

  1. 01
    Six submissions

    JavaScript quickly calls async pbkdf2 six times.

  2. 02
    Pool queue

    Free native threads take jobs; the remaining jobs wait.

  3. 03
    Main is free

    The Event Loop can handle HTTP while native threads calculate.

  4. 04
    First wave

    With the default pool, roughly four jobs often complete close together.

  5. 05
    Result callbacks

    Each native completion returns to the main Event Loop.

03 · CONTEXT

Where the result needs context

These details explain why similar code can sometimes produce a different trace.

01

Pool size is not physical core count

UV_THREADPOOL_SIZE=4 permits up to four pool jobs, but the OS and CPU decide how much work truly runs at once. A busy or low-core machine may not show clean waves.

02

Set the variable before Node starts

The pool is initialized early, so pass UV_THREADPOOL_SIZE in the process or container environment. Changing process.env later is not a reliable live reconfiguration.

03

A free Event Loop can still be slow

Main does not calculate PBKDF2, but native threads compete for the same CPU. A saturated shared pool also delays unrelated fs, crypto, and some DNS operations that use it.

04

Waves are an observation, not a contract

Equal iteration counts do not guarantee strict completion order. OS scheduling, CPU frequency, and other load can mix the callbacks.

01
Theory

First understand which parts of Node participate in execution.

02
Simplified code

Then remove instrumentation and focus on the central mechanism.

03
Runtime code

Finally match the model to the code that produces the live trace.

04 · Simplified code

A minimal model without instrumentation

src/demos.js · educational snippetJavaScript
for (let i = 0; i < 6; i++) {
  crypto.pbkdf2('secret', 'salt', 120_000, 32, 'sha256',
    () => console.log('ready', i)
  );
}
05 · Runtime code

The complete code executed by the scenario

This is not an alternative example: these are the functions and files used by the Run button.

ACTUAL SOURCE

The source is generated from the real server function. Scenarios using a child process or Worker include every participating file.

src/demos.js
scenario52 lines
import { pbkdf2 } from 'node:crypto';
import { performance } from 'node:perf_hooks';

async function libuvThreadPool(emit) {
  const jobs = 6;
  const iterations = 120_000;
  const startedAt = performance.now();

  emit(
    'call-stack',
    'sync',
    `Синхронно запускаем ${jobs} вызовов crypto.pbkdf2`,
  );
  emit(
    'libuv',
    'info',
    `UV_THREADPOOL_SIZE=${process.env.UV_THREADPOOL_SIZE ?? '4 (по умолчанию)'}`,
  );

  const tasks = Array.from({ length: jobs }, (_, index) => {
    const job = index + 1;
    emit('libuv-queue', 'schedule', `PBKDF2 #${job} отправлен в пул`);

    return new Promise((resolve, reject) => {
      pbkdf2('node-loop-lab', `salt-${job}`, iterations, 32, 'sha256', (error) => {
        if (error) {
          reject(error);
          return;
        }

        emit(
          'poll',
          'callback',
          `PBKDF2 #${job} вернулся через ${Math.round(performance.now() - startedAt)} мс`,
        );
        resolve();
      });
    });
  });

  emit(
    'call-stack',
    'info',
    'Главный JS-поток сразу свободен; вычисления идут в пуле libuv',
  );
  await Promise.all(tasks);
  emit(
    'result',
    'result',
    `Все ${jobs} задач завершены за ${Math.round(performance.now() - startedAt)} мс`,
  );
}

The emit(...) calls become live-trace rows. await and Promise keep the HTTP stream open until the scenario completes.

06 · DO NOT CONFUSE

Common misconceptions

The myth is on the left; the accurate model is on the right.

MYTH

The libuv pool and Worker Threads are the same.

ACTUALLY

The pool runs native API functions; a Worker runs your JavaScript in another isolate.

MYTH

A larger UV_THREADPOOL_SIZE is always faster.

ACTUALLY

After CPU saturation, more threads add contention and context switching.

MYTH

Every asynchronous Node API uses this pool.

ACTUALLY

Network sockets usually use OS readiness without one thread per operation.

MYTH

A free main thread means the load cannot affect HTTP.

ACTUALLY

CPU contention and a saturated shared pool can still increase latency even when JavaScript is not blocked.

07 · SELF-CHECK

Explain it in your own words

If you can explain the answer without quoting documentation, your mental model is starting to take shape.

  1. Why can job #5 wait while the Event Loop is free?
  2. What happens to fs.readFile when the shared pool is fully occupied?