Backend
Node.js
libuv and async IO

libuv and async IO

Overview: Understanding Synchronous and Asynchronous

Node.js relies on two core technologies to achieve its powerful non-blocking capabilities:

  • V8: The JavaScript engine that executes code.
  • libuv: A C-based library that handles asynchronous I/O, event loops, and thread pools.

Together, these tools make JavaScript capable of handling high-performance server tasks.


libuv

Libuv is a library written in C that gives Node.js its non-blocking, event-driven capabilities by managing lower-level asynchronous I/O, event loops, and thread pools. While Node.js provides asynchronous APIs, libuv steps in to handle system-level operations and uses its thread pool for tasks that would otherwise block the main thread. Libuv itself does not perform the tasks, but manages and delegates them efficiently.

Key Responsibilities of libuv

  • Provides an event-driven asynchronous I/O model
  • Uses thread pools for blocking operations
  • Manages timers, file I/O, networking, DNS, and more

Features

  • Efficient resource utilization (CPU, network)
  • Uses callback-based notifications
  • Abstracts system-level differences between Linux, macOS, and Windows

Synchronous vs Asynchronous JavaScript

Synchronous JavaScript

In synchronous programming, operations are performed one after another, with each line of code waiting for the previous one to finish before continuing. This results in a predictable, linear execution where each task is completed before the next begins.

Each line of code waits for the previous one to finish before continuing:

const data = fs.readFileSync('file.txt', 'utf8');
console.log(data);
console.log('Done');

Asynchronous JavaScript

Asynchronous programming allows multiple tasks to run independently. In asynchronous code, a task can start, and while waiting for it to finish, other tasks can continue. This non-blocking approach improves performance and responsiveness, especially in web applications.

Multiple tasks run independently; the program doesn’t wait:

fs.readFile('file.txt', 'utf8', (err, data) => {
  console.log(data);
});
console.log('Done');

What is V8?

V8 is Google’s open-source JavaScript engine, written in C++, used by Node.js to compile and run JavaScript.

  • Compiles JS to machine code
  • Manages memory and garbage collection
  • Provides heap memory for dynamic data
  • Maintains a call stack for function execution order

Example:

console.log('Start');
 
setTimeout(() => {
  console.log('Timeout callback executed');
}, 2000);
 
console.log('End');

Output:

Start
End
Timeout callback executed

Even though JavaScript is single-threaded, Node.js executes setTimeout using libuv so it doesn't block the main thread.


How libuv Works: The Event Loop

The event loop enables Node.js to perform non-blocking I/O operations using callbacks.

const fs = require('fs');
 
console.log('Start reading file');
 
fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log('File content:', data);
});
 
console.log('End reading file');

Output:

Start reading file
End reading file
File content: [content of example.txt]

Handling HTTP Requests in Node.js

const http = require('http');
 
console.log('Making HTTP request');
 
http.get('http://jsonplaceholder.typicode.com/posts/1', (res) => {
  let data = '';
  res.on('data', chunk => data += chunk);
  res.on('end', () => console.log('HTTP Response:', data));
}).on('error', err => console.log('Error:', err.message));
 
console.log('HTTP request made');

Output:

Making HTTP request
HTTP request made
HTTP Response: {...JSON data...}

Managing CPU-Intensive Tasks with libuv’s Thread Pool

const crypto = require('crypto');
 
console.log('Start encryption');
 
crypto.pbkdf2('password', 'salt', 100000, 64, 'sha512', (err, derivedKey) => {
  if (err) throw err;
  console.log('Encryption done:', derivedKey.toString('hex'));
});
 
console.log('End encryption');

Output:

Start encryption
End encryption
Encryption done: [hex value]

How libuv Interacts with the Operating System

OSUnderlying Mechanism
Linuxepoll
macOSkqueue
WindowsIO Completion Ports

libuv abstracts these details to provide a consistent interface for developers.


How V8, Main Thread, and libuv Work Together

Let’s walk through a combined example:

const fs = require('fs');
const https = require('https');
const a = 10;
const b = 91;
 
https.get('https://api.example.com', (res) => {
  console.log('HTTP response received');
});
 
setTimeout(() => {
  console.log('setTimeout');
}, 5000);
 
fs.readFile('text.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log('File data:', data);
});
 
function multiply(x, y) {
  return x * y;
}
 
const c = multiply(a, b);
console.log(c);

What Happens:

  1. Call Stack (V8): Executes multiply and console.log immediately.
  2. Heap Memory (V8): Stores variables like a, b, and c.
  3. Garbage Collection (V8): Cleans unused memory from the heap automatically.
  4. setTimeout: Passed to libuv’s timer queue.
  5. https.get: Passed to libuv’s network handler.
  6. fs.readFile: Offloaded to libuv’s thread pool.
  7. Event Loop: When async tasks complete, callbacks are queued.
  8. Callback Queue to Call Stack: Event loop pushes callbacks one by one for execution.

Diagram:

          JS Code (V8)
                |
          β”Œβ”€β”€β”€β”€ Call Stack ────┐
          β”‚                   β”‚
     β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”
     β”‚  Heap   β”‚         β”‚ GC (V8) β”‚
     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          |
          β–Ό
     libuv Event Loop
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚  Timers (setTimeout)       β”‚
     β”‚  I/O (fs, https)           β”‚
     β”‚  Thread Pool               β”‚
     β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
              β–Ό           β–Ό
         Callback Queue (Async Ops Done)
                   ↓
              Call Stack (Executes Callback)

Suggestions to Improve Your Understanding

βœ… Practice using setTimeout, fs.readFile, and crypto.pbkdf2 to observe asynchronous behavior

βœ… Monitor memory usage with process.memoryUsage() to see the heap grow and shrink

βœ… Use the Node.js Visualizer on nodejs.dev or VS Code’s debugger for a visual flow of the event loop

βœ… Keep exploring libuv source if you're interested in low-level internals

βœ… Add diagrams (PNG/SVG) for presentations or teaching


Conclusion

Node.js achieves non-blocking performance by combining:

  • V8 to execute JS code efficiently (call stack, heap, garbage collection)
  • libuv to handle async I/O, event loop, and thread pools

By understanding how these work together, you can write high-performance, responsive, and scalable backend applications.


If you found this helpful, please share this with your besti!