Skip to content
Plurafy

Node.js Event Loop and Libuv

Understanding Node.js event loop architecture and how it handles async operations

You’re building a server that needs to handle thousands of concurrent connections. How does Node.js handle this efficiently without creating a thread per connection?

Node.js uses libuv (a C library) to implement the event loop. This provides non-blocking I/O and allows Node.js to handle many concurrent operations with a single thread, making it highly efficient for I/O-bound applications.

// Node.js event loop runs through these phases:
// 1. timers: Execute setTimeout/setInterval callbacks
// 2. pending callbacks: Deferred I/O callbacks
// 3. idle, prepare: Internal use
// 4. poll: Retrieve and process I/O events
// 5. check: Execute setImmediate callbacks
// 6. close callbacks: Close event callbacks
// Demonstration of execution order
console.log('Start');
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
Promise.resolve().then(() => console.log('Promise'));
console.log('End');
// Output:
// Start
// End
// Promise
// setTimeout
// setImmediate

Solution: Understanding Blocking Operations

Section titled “Solution: Understanding Blocking Operations”
const fs = require('fs');
// ❌ Blocking - stops event loop
const data = fs.readFileSync('large-file.txt', 'utf8');
console.log('File read:', data.length);
// Other operations wait until file is read
// ✅ Non-blocking - doesn't stop event loop
fs.readFile('large-file.txt', 'utf8', (error, data) => {
if (error) throw error;
console.log('File read:', data.length);
});
console.log('This executes immediately');
// ✅ With async/await
async function readFileAsync() {
const data = await fs.promises.readFile('large-file.txt', 'utf8');
console.log('File read:', data.length);
}
readFileAsync();
console.log('This also executes immediately');
// libuv handles different I/O operations:
// - File I/O: Uses thread pool
// - Network I/O: Direct OS network APIs
// - Timers: Efficient timer management
// - Child processes: Process management
const fs = require('fs');
const path = require('path');
// File system operations use thread pool
for (let i = 0; i < 5; i++) {
fs.readFile(__filename, () => {
console.log(`File ${i} read`);
});
}
// Network operations don't use thread pool
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello World');
});
server.listen(3000, () => {
console.log('Server listening on port 3000');
// Handle multiple connections efficiently without thread pool
});
// Node.js event loop has specific behavior for microtasks
// Process object methods (microtasks)
process.nextTick(() => console.log('process.nextTick'));
// Promise callbacks (microtasks)
Promise.resolve().then(() => console.log('Promise'));
// setTimeout (macrotask)
setTimeout(() => console.log('setTimeout'), 0);
// setImmediate (check phase)
setImmediate(() => console.log('setImmediate'));
// Output order:
// process.nextTick
// Promise
// setTimeout
// setImmediate
// Key difference: process.nextTick executes before Promise callbacks
process.nextTick(() => console.log('tick1'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('tick2'));
// Output: tick1, tick2, promise
// Prevent blocking with setImmediate
async function processLargeDataset(data) {
const results = [];
for (let i = 0; i < data.length; i++) {
results.push(await processItem(data[i]));
// Allow event loop to process other operations periodically
if (i % 100 === 0) {
await new Promise(resolve => setImmediate(resolve));
}
}
return results;
}
// Alternative: Use process.nextTick for immediate deferral
function recursiveOperation(data, index = 0) {
if (index >= data.length) {
console.log('Done');
return;
}
processItem(data[index]);
// Defer next iteration to allow event loop processing
process.nextTick(() => recursiveOperation(data, index + 1));
}
// Limit concurrency
async function limitConcurrency(promises, limit) {
const results = [];
const executing = [];
for (const [i, promise] of promises.entries()) {
const p = Promise.resolve().then(() => promise());
results[i] = p;
if (promises.length >= limit) {
executing.push(p.then(() => {
executing.splice(executing.indexOf(p), 1);
}));
if (executing.length >= limit) {
await Promise.race(executing);
}
}
}
await Promise.all(results);
return results;
}
// Identify event loop blocking
const slowFunction = () => {
const start = performance.now();
// Simulate CPU-intensive work
for (let i = 0; i < 1000000000; i++) {
Math.sqrt(i);
}
const duration = performance.now() - start;
if (duration > 100) {
console.warn(`Warning: Event loop blocked for ${duration}ms`);
}
};
// Monitor event loop lag
let lastCheck = Date.now();
setInterval(() => {
const now = Date.now();
const lag = now - lastCheck - 1000;
if (lag > 50) {
console.warn(`Event loop lag: ${lag}ms`);
}
lastCheck = now;
}, 1000);
// Use --expose-gc flag and 'perf_hooks' module for detailed analysis
const perf_hooks = require('perf_hooks');
const obs = new perf_hooks.PerformanceObserver((items) => {
items.getEntries().forEach((entry) => {
console.log(`${entry.name}: ${entry.duration}ms`);
});
});
obs.observe({ entryTypes: ['measure'] });

What happens if you never yield to the event loop?

// This blocks the entire event loop
function infiniteLoop() {
while (true) {
// No yield to event loop
// Server becomes unresponsive
}
}
// Even this blocks longer than desired
while (true) {
process.nextTick(() => {}); // Never yields to other phases
}
// Solution: Always ensure operations complete quickly
// or use workers for CPU-intensive work