Node.js Architecture Explained: Event Loop, Async Code, and Scaling

Understanding Node.js architecture is crucial for building scalable applications. Let’s dive into the event loop, async operations, and how Node.js handles concurrency. The Event Loop Node.js uses a single-threaded event loop to handle asynchronous operations efficiently. Event Loop Phases // Simplified event loop phases ┌───────────────────────────┐ │ ┌─────────────────────┐ │ │ │ Timers │ │ // setTimeout, setInterval │ └─────────────────────┘ │ │ ┌─────────────────────┐ │ │ │ Pending Callbacks │ │ // I/O callbacks │ └─────────────────────┘ │ │ ┌─────────────────────┐ │ │ │ Idle, Prepare │ │ // Internal use │ └─────────────────────┘ │ │ ┌─────────────────────┐ │ │ │ Poll │ │ // Fetch new I/O events │ └─────────────────────┘ │ │ ┌─────────────────────┐ │ │ │ Check │ │ // setImmediate callbacks │ └─────────────────────┘ │ │ ┌─────────────────────┐ │ │ │ Close Callbacks │ │ // socket.on('close') │ └─────────────────────┘ │ └───────────────────────────┘ How It Works // Example: Understanding execution order console.log('1'); setTimeout(() => console.log('2'), 0); Promise.resolve().then(() => console.log('3')); console.log('4'); // Output: 1, 4, 3, 2 // Why: // - Synchronous code runs first (1, 4) // - Microtasks (Promises) run before macrotasks (setTimeout) // - Event loop processes callbacks Asynchronous Operations Callbacks // Traditional callback fs.readFile('file.txt', (err, data) => { if (err) { console.error(err); return; } console.log(data); }); Promises // Promise-based fs.promises.readFile('file.txt') .then(data => console.log(data)) .catch(err => console.error(err)); // Async/await async function readFile() { try { const data = await fs.promises.readFile('file.txt'); console.log(data); } catch (err) { console.error(err); } } Event Emitters const EventEmitter = require('events'); class MyEmitter extends EventEmitter {} const myEmitter = new MyEmitter(); myEmitter.on('event', (data) => { console.log('Event received:', data); }); myEmitter.emit('event', 'Hello World'); Concurrency Model Single-Threaded but Non-Blocking // Node.js handles I/O asynchronously const http = require('http'); // This doesn't block http.get('http://example.com', (res) => { res.on('data', (chunk) => { console.log(chunk); }); }); // This continues immediately console.log('Request sent, continuing...'); Worker Threads for CPU-Intensive Tasks // For CPU-intensive operations const { Worker, isMainThread, parentPort } = require('worker_threads'); if (isMainThread) { const worker = new Worker(__filename); worker.postMessage('Start calculation'); worker.on('message', (result) => { console.log('Result:', result); }); } else { parentPort.on('message', (msg) => { // Heavy computation const result = performHeavyCalculation(); parentPort.postMessage(result); }); } Scaling Strategies Cluster Module const cluster = require('cluster'); const os = require('os'); if (cluster.isMaster) { const numWorkers = os.cpus().length; for (let i = 0; i < numWorkers; i++) { cluster.fork(); } cluster.on('exit', (worker) => { console.log(`Worker ${worker.id} died`); cluster.fork(); // Restart worker }); } else { // Worker process require('./server.js'); } Load Balancing // PM2 for process management // pm2 start app.js -i max // Or use nginx/HAProxy for load balancing Best Practices 1. Avoid Blocking the Event Loop // Bad: Blocks event loop function heavyComputation() { let result = 0; for (let i = 0; i < 10000000000; i++) { result += i; } return result; } // Good: Use worker threads or break into chunks function asyncHeavyComputation() { return new Promise((resolve) => { setImmediate(() => { // Process in chunks resolve(computeChunk()); }); }); } 2. Use Streams for Large Data // Bad: Loads entire file into memory const data = fs.readFileSync('large-file.txt'); // Good: Stream processing const stream = fs.createReadStream('large-file.txt'); stream.on('data', (chunk) => { processChunk(chunk); }); 3. Handle Errors Properly // Always handle errors in async operations async function fetchData() { try { const data = await fetch('http://api.example.com'); return data; } catch (error) { // Log and handle error console.error('Fetch failed:', error); throw error; } } Performance Tips 1. Connection Pooling // Database connection pooling const pool = mysql.createPool({ connectionLimit: 10, host: 'localhost', user: 'user', password: 'password', database: 'mydb' }); 2. Caching // Use Redis for caching const redis = require('redis'); const client = redis.createClient(); async function getCachedData(key) { const cached = await client.get(key); if (cached) return JSON.parse(cached); const data = await fetchData(); await client.setex(key, 3600, JSON.stringify(data)); return data; } 3. Compression // Enable gzip compression const compression = require('compression'); app.use(compression()); Common Pitfalls 1. Callback Hell // Bad: Nested callbacks fs.readFile('file1.txt', (err, data1) => { fs.readFile('file2.txt', (err, data2) => { fs.readFile('file3.txt', (err, data3) => { // Deep nesting }); }); }); // Good: Use async/await async function readFiles() { const [data1, data2, data3] = await Promise.all([ fs.promises.readFile('file1.txt'), fs.promises.readFile('file2.txt'), fs.promises.readFile('file3.txt') ]); } 2. Unhandled Promise Rejections // Always handle promise rejections process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection:', reason); // Log and handle }); Conclusion Node.js architecture is powerful because: ...

December 10, 2025 · 4092 views

Node.js Event Loop Internals (2024)

Phases refresher timers → pending → idle/prepare → poll → check → close callbacks. Microtasks (Promises/queueMicrotask) run after each phase; process.nextTick runs before microtasks. Pitfalls Long JS on main thread blocks poll → delays I/O; move CPU work to worker threads. nextTick storms can starve I/O; prefer setImmediate when deferring. Unhandled rejections crash (from Node 15+ default); handle globally in prod. Debugging NODE_DEBUG=async_hooks or --trace-events-enabled --trace-event-categories node.async_hooks. Measure event loop lag: perf_hooks.monitorEventLoopDelay(). Profile CPU with node --inspect + Chrome DevTools; use flamegraphs for hotspots. Practices Limit synchronous JSON/crypto/zlib; offload to worker threads or native modules. Keep microtask chains short; avoid deep promise recursion. Use AbortController for cancellable I/O; always clear timers. Checklist Monitor event loop lag & heap usage. Worker pool sized for CPU tasks; main loop kept light. Errors and rejections centrally handled; graceful shutdown in place.

September 18, 2024 · 3677 views