# one thread, four helpers

2022-02-15 · 3 min read · #nodejs

Source: https://jopcmelo.com/writing/one-thread-four-helpers

is Node.js single-threaded? **your JavaScript is. Node isn't.**

## the event loop

your code runs on one thread, in a loop. the [event loop](https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick) goes around in phases: run the timers that are due, wait for I/O and run its callbacks, run the `setImmediate` callbacks, run the close callbacks, and around again (there are two more phases, but they're internal). one callback at a time, never two at once.

*(sketch: the event loop as a circle with four stops: timers, poll, check and close, with your code in the middle. a marker goes around it, stopping at each. beside it, a thread pool with four threads: file system, DNS, crypto and zlib work goes there, and a callback comes back)*

so if one callback takes long, everything else waits. this asks for a 10ms timer, then adds up a billion numbers:

```javascript
const start = performance.now();
setTimeout(() => {
  console.log(`asked for 10ms, ran at ${Math.round(performance.now() - start)}ms`);
}, 10);

let n = 0;
for (let i = 0; i < 1_000_000_000; i++) n += i;
```

on my machine (Node 24, a Ryzen 7 3700X) it printed `asked for 10ms, ran at 747ms`, twice in a row. **the timer was ready at 10ms, but the loop was busy adding.**

*(sketch: a timeline. a long block, adding a billion numbers, runs from 0 to 747ms. the timer was ready at 10ms, and only runs after the block ends)*

## where the other threads are

reading a file or hashing a password doesn't happen on that thread. Node hands it to [libuv](https://docs.libuv.org/en/v1.x/threadpool.html), the C library underneath it, which keeps a pool of threads for the work the operating system can't do without blocking. per [Node's own guide](https://nodejs.org/en/learn/asynchronous-work/dont-block-the-event-loop), that's the file system, `dns.lookup`, the expensive parts of `crypto` and `zlib`. network sockets never go there: the operating system tells libuv when they're ready.

**the pool has four threads**, unless you set `UV_THREADPOOL_SIZE` (up to 1024). you can watch the four of them work:

```javascript
import { pbkdf2 } from 'node:crypto';

const start = performance.now();
for (let i = 1; i <= 8; i++) {
  pbkdf2('password', 'salt', 1_000_000, 64, 'sha512', () => {
    console.log(`hash ${i} done at ${Math.round(performance.now() - start)}ms`);
  });
}
```

eight hashes, four threads: the first four finished between 463 and 473ms, the next four between 929 and 947ms. with `UV_THREADPOOL_SIZE=8`, all eight were done by 675ms, since my CPU has 16 threads to put them on.

*(sketch: eight hashes as bars on a timeline. on four threads they run in two waves, done around 470ms and 940ms. on eight threads, they all run at once and finish by 675ms)*

## threads of your own

if your own JavaScript is the slow part, [`worker_threads`](https://nodejs.org/api/worker_threads.html) runs it on another thread, with its own event loop, talking to the main one through messages. that's as close as Node gets to multi-threaded JavaScript, and you have to ask for it.

so: one thread for your code, four helpers underneath, more if you ask.
