NodeJSlibuv & the C++ Core

libuv & the C++ Core

If V8 is the part of Node.js that runs your JavaScript, libuv is the part that lets that JavaScript talk to the operating system without blocking. It is a multi-platform C library, originally written specifically for Node.js (and now used by other projects too), that provides three things Node depends on completely: the event loop, asynchronous I/O, and a thread pool. Almost every asynchronous thing you do in Node — reading a file, accepting a connection, resolving a hostname, hashing a password — passes through libuv.

What libuv provides

Feature

What it does

Event loop

The core loop that schedules and runs callbacks — see The Event Loop

Async network I/O

TCP, UDP, pipes, using the OS's most efficient event mechanism

Async file I/O

File operations, run on the thread pool

Thread pool

A small pool of worker threads for work the OS cannot do async

DNS

getaddrinfo/getnameinfo, on the thread pool

Timers

Backing for setTimeout / setInterval

Signals & child processes

OS signal handling and process spawning

The two paths of asynchrony

This is the single most important — and most often skipped — detail about libuv. It handles asynchronous work in two fundamentally different ways, and knowing which path an operation takes explains a lot of real-world performance behaviour.

Path

Used for

Cost

Why

Kernel-offloaded

Network I/O (TCP/UDP/HTTP)

No extra thread

The OS notifies libuv when a socket is ready — true async at the kernel level

Thread-pool-offloaded

Files, DNS lookup, crypto, zlib

One pool thread per op

The OS has no good async API for these, so libuv simulates async by running them on background threads

Under the hood, the kernel path uses the most efficient event-notification mechanism each platform offers:

  • Linuxepoll

  • macOS / BSDkqueue

  • Windows — IOCP (I/O Completion Ports)

  • Solaris — event ports

Why network scaling is nearly free
Because network sockets are watched by the **kernel**, ten thousand idle connections consume essentially no Node threads — the OS simply tells libuv which sockets have data. This is the mechanism behind Node's famous ability to handle huge numbers of concurrent connections.
The thread pool

File, DNS, and CPU-bound crypto/compression work runs on libuv's thread pool, which defaults to 4 threads. You can change its size with the UV_THREADPOOL_SIZE environment variable — but it must be set before the process starts, because the pool is created once at startup.

Bash
# Allow up to 8 concurrent file/DNS/crypto/zlib operations
UV_THREADPOOL_SIZE=8 node app.js
Thread-pool exhaustion is a real bottleneck
Because the pool is small, firing many simultaneous file reads, `crypto.pbkdf2` calls, or `zlib` operations can **saturate** it. Once all threads are busy, further requests **queue** — they appear slow even though the event loop itself is idle and CPU is available. A server that hashes passwords on login can stall under a login spike for exactly this reason.
Demonstrating the pool size

Run this with UV_THREADPOOL_SIZE=2 and then =4 and watch the timing change. With a pool of 2, the four hashes finish in two waves; with 4+, they finish together.

pool.js

JS
import crypto from 'node:crypto'

const start = Date.now()
for (let i = 0; i < 4; i++) {
  crypto.pbkdf2('secret', 'salt', 1_000_000, 64, 'sha512', () => {
    console.log(`Hash ${i + 1} done after ${Date.now() - start}ms`)
  })
}
# UV_THREADPOOL_SIZE=2 — two waves:
Hash 1 done after 410ms
Hash 2 done after 415ms
Hash 3 done after 820ms
Hash 4 done after 825ms
Where libuv sits in a full request

The round trip for a thread-pool operation

Text
Your JS (V8)
   │  fs.readFile('x', cb)
   ▼
Node C++ binding
   │
   ▼
libuv  ──►  thread pool thread does the blocking read
   │              │
   │         read completes
   ▼              ▼
event loop  ◄──  libuv queues cb
   │
   ▼
cb(err, data)  runs back in V8 on the main thread
The full chain
Your JavaScript (V8) → a Node API such as `fs.readFile` → C++ bindings → **libuv** → the OS kernel or thread pool → result → callback queued → the **event loop** runs your callback (back in V8). libuv is the engine room connecting the language to the machine.
Tuning notes
  • Raise UV_THREADPOOL_SIZE if you do heavy concurrent file/crypto/zlib work — but it does not help network throughput (that path uses no pool threads).

  • Never run a synchronous (*Sync) or CPU-bound operation on the main thread expecting libuv to save you — only the async APIs are offloaded.

  • For genuinely CPU-bound JavaScript, the thread pool will not help; use Worker Threads instead.

Next
Now connect the dots: read [Single-Threaded, Non-Blocking I/O](/nodejs/single-threaded-model) to see how V8 + libuv produce Node's concurrency model.