Node.js libuv and thread pool
libuv is the C library that gives Node its event loop, async I/O via OS primitives (epoll, kqueue, IOCP), and a 4-thread pool for things the OS cannot do async.
Node is V8 (the JS engine) plus libuv (the async I/O library) plus a few other C++ bindings. libuv is the reason Node can handle 10,000 concurrent connections on one thread.
What libuv does
libuv provides:
- An event loop with phases.
- Cross-platform async network I/O using
epoll(Linux),kqueue(macOS, BSD), orIOCP(Windows). - A thread pool (4 threads by default) for operations the OS cannot do async.
- Async DNS resolution.
- Child processes, signals, pipes, TTY, timers, FS events.
The event loop is single-threaded from JS's perspective. The thread pool is hidden behind the libuv API. Your JS code runs on the main thread; libuv runs blocking syscalls on the pool and posts the results back as events.
The phases
Each loop tick has six phases, in order:
- Timers: callbacks scheduled by
setTimeoutandsetIntervalwhose time has elapsed. - Pending callbacks: deferred I/O callbacks (a few system errors).
- Idle, prepare: internal.
- Poll: retrieve new I/O events, run their callbacks. Blocks here when there is nothing else to do.
- Check:
setImmediatecallbacks. - Close callbacks: e.g.
socket.on('close', ...).
Between every callback in every phase, two queues drain: process.nextTick first, then promise microtasks.
The thread pool
Some operations cannot be done async on every OS:
- File system (read, write, stat) on Linux, because
epolldoes not support regular files reliably. - DNS (
dns.lookupusesgetaddrinfo, which is blocking). - Crypto (
pbkdf2,scrypt,randomByteswith large sizes). zlibcompression and decompression.
These go to the libuv thread pool. Default size is 4. You can raise it with UV_THREADPOOL_SIZE=16 before Node starts.
The pool is the most common source of mysterious performance issues. Your app does 50 concurrent file reads, four run in parallel, the rest queue. Latency jumps. Profile shows JS is idle. The fix is either fewer concurrent FS ops or a larger pool.
UV_THREADPOOL_SIZE=32 node server.jsNetwork I/O does not use the pool
Sockets, HTTP, TCP, UDP all use the OS's native async I/O directly (epoll/kqueue/IOCP). No thread pool involvement. This is why Node scales to thousands of concurrent connections on a single thread.
The model that locks it in: V8 runs your JS on one thread. libuv runs the event loop on the same thread. The thread pool runs blocking syscalls behind the scenes. Network I/O bypasses the pool entirely. CPU-bound work blocks everything because there is only one JS thread.
Learn more
- Docslibuv documentationlibuv
- Docs
- TalkBert Belder: Everything you need to know about Node.js Event LoopNode.js Interactive