Event Loop & Non-blocking IO
15 questions found
What is the difference between synchronous and asynchronous versions of the same core Node.js API, using fs.readFile vs fs.readFileSync as an example, and when is the synchronous version actually appropriate?
Intermediate
The synchronous version blocks the entire event loop until the operation completes, returning its result directly rather than via a callback or promise -- generally discouraged in a server handling concurrent requests, but genuinely appropriate in specific contexts like a one-off CLI script's startup sequence (reading a config file before anything else happens, where blocking briefly at startup causes no harm since there's nothing else concurrently happening yet) or simple, single-purpose scripts with no concurrency concerns at all.
// Appropriate use of the synchronous version: reading config once at startup,
// before the server starts accepting any concurrent requests
const config = JSON.parse(fs.readFileSync('config.json', 'utf-8'));
app.listen(config.port); // only starts accepting requests after config is loaded
Real-world example
A CLI tool that runs once, processes a file, and exits uses fs.readFileSync() throughout without concern, since there's no concurrent request-handling happening that a brief synchronous block could interfere with, unlike in a running web server.
Common follow-ups: What's the specific risk of using a synchronous fs call inside a running Express route handler versus at application startup?;Are there other core Node.js APIs beyond fs that offer both sync and async variants following this same pattern?
File System (fs) Module;CLI Tools & Scripting with Node.js
How does the V8 JavaScript engine's microtask queue interact with the Node.js event loop's macrotask phases, and why does this occasionally surprise developers?
Advanced
Promise-based microtasks are actually managed by the V8 engine itself, independently of Node's own event loop implementation (which handles macrotasks like timers and I/O) -- but Node.js ensures the microtask queue is fully drained after each individual callback completes (not just once per full event loop iteration), meaning a chain of many resolved promises can, in principle, indefinitely delay macrotasks like timers from ever running if new microtasks keep being scheduled from within other microtasks, a subtle 'microtask starvation' scenario that can surprise developers expecting timers to fire predictably.
function scheduleForever() {
Promise.resolve().then(scheduleForever); // an infinite chain of microtasks
}
scheduleForever();
setTimeout(() => console.log('This may never fire!'), 100); // starved by the microtask chain above
Real-world example
A developer debugging why a setTimeout callback never fired despite a generous delay eventually traces it to an accidental infinite chain of chained .then() calls recursively scheduling more microtasks, which as V8-level microtasks are always fully drained before the event loop can proceed to the timers phase.
Common follow-ups: What's the practical difference between this microtask-starvation scenario and the earlier-discussed risk of process.nextTick() recursion starving I/O?;How would you detect this kind of starvation happening in a production application experiencing mysteriously delayed timers?
Async Patterns;Debugging & Diagnostics
What is the difference between concurrency and parallelism, and which one does Node.js's event loop actually provide by default?
Beginner
Concurrency means multiple tasks are in progress and making incremental progress over the same time period, potentially by interleaving execution, without necessarily running at the exact same instant. Parallelism means multiple tasks execute at literally the same instant on separate CPU cores. Node.js's single-threaded event loop provides concurrency for I/O-bound work by default (juggling many pending operations on one thread), but not true parallelism, which requires explicitly using worker threads, child processes, or the cluster module to actually utilize multiple CPU cores simultaneously.
// Concurrency: many I/O operations in flight on one thread, interleaved
Promise.all([fetchA(), fetchB(), fetchC()]); // concurrent, not parallel
// True parallelism requires worker threads or multiple processes
const worker = new Worker('./cpu-task.js'); // runs on an actual separate thread
Real-world example
A team correctly explains to a new hire that their Node.js API handling thousands of simultaneous connections achieves this through concurrency on a single thread, not parallelism, and that CPU-bound work genuinely needing to run in parallel across cores requires deliberately reaching for worker threads instead.
Common follow-ups: Why is this distinction important when explaining Node.js's scalability characteristics to someone coming from a multi-threaded language background?;How does the cluster module provide a form of parallelism despite each individual worker still being single-threaded internally?
Clustering & Worker Threads;Advanced Node.js
How would you measure how 'busy' the event loop is in a running Node.js process using a simple technique?
Intermediate
A basic technique schedules a callback with a known delay (like setImmediate or a short setTimeout) and measures the actual elapsed time versus the expected delay -- if the event loop is busy processing other work, the callback fires later than scheduled, and that measured difference (the 'lag') serves as a rough real-time indicator of how backed up or blocked the event loop currently is, forming the basis of tools like the 'toobusy' or 'blocked-at' packages.
function measureLoopLag(callback) {
const start = process.hrtime.bigint();
setImmediate(() => {
const lagNs = process.hrtime.bigint() - start;
callback(Number(lagNs) / 1e6); // lag in milliseconds
});
}
measureLoopLag((lagMs) => console.log('Event loop lag:', lagMs, 'ms'));
Real-world example
A health-check endpoint includes a quick event-loop-lag measurement, returning an unhealthy status if the lag exceeds a threshold, letting a load balancer or orchestrator temporarily route traffic away from an instance that's become overloaded and unresponsive.
Common follow-ups: How does this simple technique compare to the more precise, built-in monitorEventLoopDelay from perf_hooks discussed earlier?;What threshold of lag would reasonably indicate an instance should stop receiving new traffic?
Performance Optimization & Profiling;Debugging & Diagnostics
What is a callback queue (or task queue) in the context of the Node.js event loop, and how does it relate to the call stack?
Beginner
The call stack is where JavaScript's currently executing synchronous code lives, executed one frame at a time. When an asynchronous operation completes, its associated callback isn't executed immediately -- it's placed into an appropriate queue (a macrotask queue for things like I/O and timers, or the microtask queue for promises), and the event loop only moves a queued callback onto the call stack once the stack is completely empty, ensuring currently running synchronous code always finishes before any queued asynchronous callback begins.
console.log('1');
setTimeout(() => console.log('3'), 0); // queued, waits for the call stack to empty
console.log('2');
// Output: 1, 2, 3 -- the setTimeout callback only runs once the synchronous code finishes
// and the call stack is empty, even with a 0ms delay
Real-world example
A developer confused why a setTimeout(fn, 0) callback doesn't run immediately learns that it's placed in a queue and can only execute once the call stack is completely empty, which is why any additional synchronous code after the setTimeout call still runs first, regardless of the specified delay.
Common follow-ups: Why does even a 0ms setTimeout still get queued rather than running synchronously and immediately?;How many different queues does Node.js actually have, given microtasks and macrotasks are handled somewhat differently?
Advanced Node.js;Async Patterns