DEV Community

Krati Joshi
Krati Joshi

Posted on

๐Ÿš€ Backend Internals #7: libuv โ€” The Real Reason Node.js Is Asynchronous

By Krati Joshi

Most developers say:

"Node.js is single-threaded."

That's trueโ€”but it's also incomplete.

If JavaScript runs on a single thread, then who reads files?
Who handles thousands of HTTP connections?
Who performs DNS lookups and cryptographic operations without freezing your application?

The answer is libuv.

Understanding libuv is the moment Node.js starts making sense.

Let's dive in.


The Problem

Imagine this code:

const fs = require("fs");

const data = fs.readFileSync("movie.mp4");

console.log("Done");
Enter fullscreen mode Exit fullscreen mode

If movie.mp4 is 5 GB, your application waits until the file is completely read.

Nothing else executes.

This is called blocking I/O.

Now imagine an Express server handling 500 users.

If every request performs blocking file operations, your server quickly becomes unresponsive.

Clearly, Node.js needed a better approach.


Enter libuv

libuv is a cross-platform C library used internally by Node.js.

Its job is to provide:

  • Asynchronous I/O
  • Event Loop implementation
  • Worker Thread Pool
  • Timers
  • File System APIs
  • Networking abstraction
  • DNS operations

Without libuv, Node.js would not be able to perform asynchronous operations efficiently.


Where Does libuv Fit?

Every Node.js program roughly follows this architecture:

JavaScript Code
        โ”‚
        โ–ผ
     V8 Engine
        โ”‚
        โ–ผ
Node.js Native C++ Bindings
        โ”‚
        โ–ผ
      libuv
   โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”
   โ”‚         โ”‚
   โ–ผ         โ–ผ
Thread Pool  Operating System
             Async I/O APIs
Enter fullscreen mode Exit fullscreen mode

Each component has a different responsibility.

V8

  • Executes JavaScript
  • Performs JIT compilation
  • Manages memory and garbage collection

Node.js Native Bindings

Expose APIs like:

  • fs
  • http
  • crypto
  • dns

to JavaScript.

libuv

Coordinates asynchronous work with the operating system.


Why JavaScript Can't Read Files

JavaScript was originally designed for browsers.

It has no built-in ability to:

  • Read files
  • Create sockets
  • Open network connections
  • Access operating system APIs

When you write:

fs.readFile("notes.txt", callback);
Enter fullscreen mode Exit fullscreen mode

JavaScript doesn't read the file itself.

Instead, the request travels through Node.js into libuv, which performs the work using the operating system or its worker thread pool.


The Flow of fs.readFile()

Internally, the request looks like this:

JavaScript

โ†“

V8 Engine

โ†“

Node.js Native Bindings

โ†“

libuv

โ†“

Worker Thread

โ†“

Operating System

โ†“

File Read Complete

โ†“

Completion Queue

โ†“

Event Loop

โ†“

JavaScript Callback
Enter fullscreen mode Exit fullscreen mode

Notice something important:

The JavaScript thread never waits.

It simply continues executing other work until the callback is ready.

That's the foundation of Node.js's non-blocking model.


The Thread Pool

Some operations cannot rely entirely on the operating system's asynchronous APIs.

For these, libuv maintains a Worker Thread Pool.

By default:

4 Worker Threads
Enter fullscreen mode Exit fullscreen mode

Typical operations that use the thread pool include:

  • File System (fs)
  • Cryptography (crypto)
  • Compression (zlib)
  • Some DNS lookups (dns.lookup())

The pool size can be increased when appropriate:

UV_THREADPOOL_SIZE=8 node app.js
Enter fullscreen mode Exit fullscreen mode

This can improve throughput for workloads dominated by thread-pool operations like hashing or compression.


Not Every Async Operation Uses the Thread Pool

One of the biggest interview misconceptions is:

"All asynchronous work uses the thread pool."

It doesn't.

Uses Thread Pool

  • fs.readFile()
  • crypto.pbkdf2()
  • crypto.scrypt()
  • zlib
  • dns.lookup()

Doesn't Use Thread Pool

  • HTTP requests
  • TCP sockets
  • Timers
  • Most network I/O

Networking is usually handled using efficient operating-system mechanisms, while libuv waits for readiness notifications and schedules callbacks.


Why Can Node.js Handle Thousands of Connections?

Unlike many traditional server models, Node.js does not create one thread per connection.

Instead:

Thousands of Clients

โ†“

Operating System

โ†“

libuv Event Loop

โ†“

JavaScript Callback
Enter fullscreen mode Exit fullscreen mode

Most connections spend their time waiting for network events.

Because JavaScript isn't blocked waiting for I/O, one thread can coordinate a huge number of concurrent connections.


Event Loop vs Thread Pool

These two concepts are often confused.

Event Loop Thread Pool
Executes callbacks Performs supported blocking work
Runs on the JavaScript thread Runs on worker threads
Single-threaded Multi-threaded
Schedules completed tasks Executes background operations

The Event Loop doesn't read files.

The Thread Pool doesn't execute JavaScript.

They work together.


Common Interview Questions

Why is Node.js called single-threaded?

Because JavaScript execution happens on a single main thread.

Node.js itself also uses libuv, worker threads, and operating-system asynchronous I/O.


Does every async API use the thread pool?

No.

Only certain APIs such as fs, crypto, zlib, and some DNS operations use it.

Most networking operations rely on the operating system's asynchronous I/O facilities.


Why is libuv written in C?

Because it needs direct access to low-level operating system APIs while providing a consistent interface across Linux, Windows, and macOS.


Key Takeaways

  • JavaScript execution is single-threaded.
  • libuv powers Node.js asynchronous behavior.
  • libuv implements the Event Loop and manages the worker thread pool.
  • File system, crypto, compression, and some DNS operations use the thread pool.
  • HTTP requests and most networking do not use the thread pool.
  • Understanding libuv helps explain why Node.js scales well for I/O-heavy applications.

If you've ever wondered why Node.js can serve thousands of concurrent requests without creating thousands of threads, the answer isn't magicโ€”it's the combination of libuv, the operating system, and the Event Loop working together.

In the next article, we'll explore how the Node.js HTTP module works internally and see what actually happens when a browser sends a request to your server.

Top comments (0)