Debugs native module crashes, optimizes V8 performance, configures node-gyp builds, writes N-API/node-addon-api bindings, and diagnoses libuv event loop issues in Node.js. Use when working with C++ addons, native modules, binding.gyp, node-gyp errors, segfaults, memory leaks in native code, V8 optimization/deoptimization, libuv thread pool tuning, N-API or NAN bindings, build system failures, or any Node.js internals below the JavaScript layer.
99
99%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
libuv uses a thread pool for operations that can't be performed asynchronously at the OS level. Understanding the thread pool is essential for avoiding bottlenecks.
The thread pool handles:
fs.* except FSWatcher)dns.lookup(), not dns.resolve*())crypto.pbkdf2(), crypto.randomBytes())uv_queue_workMain Thread (Event Loop)
│
├──> Timer callbacks (no thread pool)
├──> Network I/O (no thread pool - uses epoll/kqueue/IOCP)
│
└──> Thread Pool (blocking ops)
├── Thread 1: fs.readFile()
├── Thread 2: dns.lookup()
├── Thread 3: crypto.pbkdf2()
└── Thread 4: zlib.gzip()The default thread pool size is 4 threads.
# Check default
node -e "console.log(process.env.UV_THREADPOOL_SIZE || 4)"This means only 4 blocking operations can execute concurrently!
Configure the pool size at startup:
# Increase thread pool (must be set before Node.js starts)
UV_THREADPOOL_SIZE=16 node app.js
# Maximum is 1024
UV_THREADPOOL_SIZE=128 node app.js// WRONG: Setting after Node.js starts has no effect
process.env.UV_THREADPOOL_SIZE = 16; // Too late!
// Must be set before Node.js initialization:
// - In shell: export UV_THREADPOOL_SIZE=16
// - In package.json scripts: "start": "UV_THREADPOOL_SIZE=16 node app.js"
// - In systemd: Environment=UV_THREADPOOL_SIZE=16const fs = require('node:fs/promises');
const dns = require('node:dns/promises');
// With default pool size of 4:
async function handleRequest(hostname) {
// These ALL use the thread pool:
const [
file1,
file2,
file3,
file4,
resolved // This waits for a free thread!
] = await Promise.all([
fs.readFile('config1.json'),
fs.readFile('config2.json'),
fs.readFile('config3.json'),
fs.readFile('config4.json'),
dns.lookup(hostname) // Blocked until a thread is free
]);
}const dns = require('node:dns');
// DNS lookup is a good canary for thread pool saturation
function measureThreadPoolLatency() {
const start = process.hrtime.bigint();
dns.lookup('localhost', (err) => {
const end = process.hrtime.bigint();
const ms = Number(end - start) / 1e6;
if (ms > 10) {
console.warn(`Thread pool latency: ${ms.toFixed(2)}ms`);
}
});
}
setInterval(measureThreadPoolLatency, 1000);const async_hooks = require('node:async_hooks');
const fs = require('node:fs');
// Track thread pool operations
const threadPoolTypes = new Set([
'FSREQCALLBACK',
'FSREQPROMISE',
'GETADDRINFOREQWRAP',
'GETNAMEINFOREQWRAP',
'PBKDF2REQUEST',
'RANDOMBYTESREQUEST',
'SCRYPTREQUEST',
'SIGNREQUEST',
'VERIFYREQUEST',
'ZLIB'
]);
let activeThreadPoolOps = 0;
let maxConcurrent = 0;
const hook = async_hooks.createHook({
init(asyncId, type) {
if (threadPoolTypes.has(type)) {
activeThreadPoolOps++;
maxConcurrent = Math.max(maxConcurrent, activeThreadPoolOps);
}
},
destroy(asyncId, type) {
// Note: type not available in destroy, need to track separately
}
});
hook.enable();
setInterval(() => {
console.log(`Active thread pool ops: ${activeThreadPoolOps}, max: ${maxConcurrent}`);
maxConcurrent = activeThreadPoolOps;
}, 5000);const fs = require('node:fs');
// ALL of these use the thread pool:
fs.readFile('file.txt', callback);
fs.writeFile('file.txt', data, callback);
fs.stat('file.txt', callback);
fs.readdir('.', callback);
fs.open('file.txt', 'r', callback);
// Even metadata operations!
// Exception: fs.watch() / fs.watchFile() use OS facilities
fs.watch('.', (event, filename) => {
// This does NOT use thread pool
});const dns = require('node:dns');
// Uses thread pool (calls getaddrinfo)
dns.lookup('example.com', callback);
// Does NOT use thread pool (uses c-ares)
dns.resolve('example.com', callback);
dns.resolve4('example.com', callback);
dns.resolveMx('example.com', callback);Recommendation: Prefer dns.resolve*() for high-throughput:
const dns = require('node:dns');
// BAD: Thread pool bottleneck
async function resolveMany(hostnames) {
return Promise.all(
hostnames.map(h => dns.promises.lookup(h))
);
}
// GOOD: Uses c-ares, no thread pool
async function resolveMany(hostnames) {
return Promise.all(
hostnames.map(h => dns.promises.resolve4(h))
);
}const crypto = require('node:crypto');
// Uses thread pool:
crypto.pbkdf2(password, salt, iterations, keylen, 'sha512', callback);
crypto.randomBytes(256, callback);
crypto.scrypt(password, salt, keylen, callback);
// Does NOT use thread pool (runs on main thread):
crypto.createHash('sha256').update(data).digest();
crypto.createCipheriv(algorithm, key, iv);const zlib = require('node:zlib');
// Uses thread pool:
zlib.gzip(buffer, callback);
zlib.gunzip(buffer, callback);
zlib.deflate(buffer, callback);
zlib.inflate(buffer, callback);
// Sync versions block main thread (avoid!):
zlib.gzipSync(buffer); // BAD// Optimal size depends on:
// 1. Number of CPU cores
// 2. Nature of blocking operations
// 3. Concurrent request load
const os = require('node:os');
// For I/O-heavy workloads:
// More threads than CPUs is fine (threads are often waiting)
const ioHeavySize = Math.max(os.cpus().length * 2, 4);
// For CPU-heavy workloads (crypto):
// Match CPU count to avoid context switching
const cpuHeavySize = os.cpus().length;
// For mixed workloads:
// Balance between I/O wait and CPU usage
const mixedSize = Math.max(os.cpus().length * 1.5, 4);const { monitorEventLoopDelay } = require('node:perf_hooks');
// Monitor event loop delay
const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();
// If p99 latency is high, thread pool may be saturated
setInterval(() => {
const p99 = histogram.percentile(99) / 1e6;
if (p99 > 100) {
console.warn(`Event loop p99: ${p99.toFixed(2)}ms - consider increasing UV_THREADPOOL_SIZE`);
}
histogram.reset();
}, 10000);// Instead of file system for caching:
// Use Redis, Memcached, or in-memory cache
const { LRUCache } = require('lru-cache');
const cache = new LRUCache({ max: 500 });
// Instead of dns.lookup():
// Use dns.resolve4() with custom caching
const dnsCache = new Map();
async function cachedResolve(hostname) {
if (dnsCache.has(hostname)) {
return dnsCache.get(hostname);
}
const addresses = await dns.promises.resolve4(hostname);
dnsCache.set(hostname, addresses[0]);
// Expire after 5 minutes
setTimeout(() => dnsCache.delete(hostname), 5 * 60 * 1000);
return addresses[0];
}const { Worker, isMainThread, parentPort } = require('node:worker_threads');
if (isMainThread) {
// Main thread: dispatch work to workers
const worker = new Worker(__filename);
worker.postMessage({ password: 'secret', salt: 'random' });
worker.on('message', (hash) => {
console.log('Hash:', hash);
});
} else {
// Worker thread: do CPU-heavy work
const crypto = require('node:crypto');
parentPort.on('message', ({ password, salt }) => {
// This runs in worker, not thread pool
const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512');
parentPort.postMessage(hash.toString('hex'));
});
}const fs = require('node:fs');
const { pipeline } = require('node:stream/promises');
// BAD: Holds thread pool slot for entire read
const data = await fs.promises.readFile('huge-file.txt');
// BETTER: Streaming uses thread pool in small chunks
await pipeline(
fs.createReadStream('huge-file.txt'),
processStream,
fs.createWriteStream('output.txt')
);When writing C++ addons, use uv_queue_work for blocking operations:
#include <napi.h>
#include <uv.h>
struct WorkData {
std::string input;
std::string result;
Napi::ThreadSafeFunction tsfn;
};
// Runs on thread pool thread
void Execute(uv_work_t* req) {
WorkData* data = static_cast<WorkData*>(req->data);
// Do blocking work here
data->result = expensiveOperation(data->input);
}
// Runs on main thread after Execute completes
void Complete(uv_work_t* req, int status) {
WorkData* data = static_cast<WorkData*>(req->data);
data->tsfn.BlockingCall([data](Napi::Env env, Napi::Function callback) {
callback.Call({Napi::String::New(env, data->result)});
});
data->tsfn.Release();
delete data;
delete req;
}
Napi::Value QueueWork(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
auto* data = new WorkData();
data->input = info[0].As<Napi::String>().Utf8Value();
data->tsfn = Napi::ThreadSafeFunction::New(
env, info[1].As<Napi::Function>(), "work", 0, 1
);
auto* req = new uv_work_t();
req->data = data;
uv_queue_work(uv_default_loop(), req, Execute, Complete);
return env.Undefined();
}Increase pool size for I/O-heavy apps: UV_THREADPOOL_SIZE=16 or more
Use dns.resolve*() instead of dns.lookup() when possible
Monitor thread pool saturation with async hooks or custom metrics
Stream large files instead of reading entirely
Use worker threads for CPU-intensive operations
Cache DNS results to reduce thread pool usage
Consider async alternatives (Redis, network services) over file I/O
deps/uv/src/threadpool.c in Node.js sourcedns documentation for lookup vs resolve differencesrules