Event Loop Basics
9 examples to get you started with the Event Loop - 6 basic and 3 intermediate.
Prerequisites
- Node.js 24.18.0 installed.
- Read How Node.js Works for the V8 + libuv overview.
Basic Examples
1. Call Stack Order
Synchronous code runs to completion before any scheduled callback.
console.log('1');
console.log('2');
console.log('3');
// Output: 1, 2, 3- The call stack executes one frame at a time - LIFO order.
- Nothing async runs until the stack is empty.
- Long synchronous functions delay all pending I/O callbacks.
Related: How Node.js Works - V8 and the single thread
2. setTimeout Schedules a Macrotask
setTimeout does not run immediately - it queues a callback for a later loop iteration.
console.log('start');
setTimeout(() => console.log('timeout'), 0);
console.log('end');
// start, end, timeout0ms is a minimum delay, not a guarantee - the loop must clear first.- Timers run in the timers phase of libuv.
- Heavy sync work between
setTimeoutand its fire delays the callback further.
Related: Timers & Scheduling - drift under load
3. Promise Microtasks Run Before Timers
Microtasks (Promise .then) drain before the next macrotask.
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
// A, D, C, BPromise.resolve().thenschedules a microtask on the microtask queue.- Node drains all microtasks before moving to the next macrotask phase.
- This ordering explains many "why did X run before Y?" bugs.
Related: Microtasks vs Macrotasks - full ordering rules
4. await Yields to the Event Loop
await suspends the async function and resumes via microtasks.
async function demo(): Promise<void> {
console.log('before');
await Promise.resolve();
console.log('after');
}
demo();
console.log('sync');
// before, sync, after- Code after
awaitruns as a microtask continuation. - Other microtasks and sync code can interleave before
after. - Nested
awaitchains stack microtasks - keep chains shallow when possible.
Related: async/await in Node - error handling patterns
5. Non-Blocking File Read
fs.promises schedules I/O on libuv and returns control immediately.
import { readFile } from 'node:fs/promises';
console.log('before read');
readFile('package.json', 'utf8').then((data) => {
console.log('read', data.length, 'bytes');
});
console.log('after read');- The
.thencallback runs after the file read completes on a background thread. - JavaScript between
beforeandafterruns while I/O is in flight. readFileSyncwould block the loop - avoid it in request paths.
6. setImmediate vs setTimeout(0)
setImmediate runs in the check phase, after I/O polling.
import { setImmediate } from 'node:timers';
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// Order varies at top level; inside I/O callbacks, immediate usually wins- At module top level, order between
setTimeout(0)andsetImmediateis not guaranteed. - Inside an I/O callback,
setImmediatetypically runs before the next timer. - Prefer
queueMicrotaskorsetImmediateoversetTimeout(fn, 0)for deferral.
Related: libuv Phases - timers, poll, check phases
Intermediate Examples
7. Blocking the Event Loop
CPU work on the main thread stalls all other callbacks.
import { createServer } from 'node:http';
function block(ms: number): void {
const end = Date.now() + ms;
while (Date.now() < end) { /* spin */ }
}
createServer((req, res) => {
if (req.url === '/block') block(3000);
res.end('ok');
}).listen(3000);- While
blockruns, no other requests progress in this process. - Load tests show p99 spikes even when mean latency looks fine.
- Offload CPU work to
worker_threadsor a job queue.
Related: Detecting Event-Loop Blockage - measure lag in production
8. process.nextTick Priority
nextTick runs before other microtasks - use sparingly.
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
// nextTick, promisenextTickqueue drains before Promise microtasks.- Recursive
nextTickcan starve I/O - never loop withnextTick. - Prefer
queueMicrotaskfor deferral unless you need nextTick semantics.
Related: Microtasks vs Macrotasks - nextTick ordering
9. Measure Event Loop Utilization
perf_hooks.eventLoopUtilization reports how busy the loop is.
import { eventLoopUtilization } from 'node:perf_hooks';
const start = eventLoopUtilization();
setInterval(() => {
const elu = eventLoopUtilization(start);
console.log('utilization:', elu.utilization.toFixed(3));
}, 5000);- Utilization near 1.0 means the loop is saturated - scale or optimize.
- Combine with HTTP latency metrics to correlate user pain with loop health.
- Export as a Prometheus gauge for alerting.
Related: Event Loop Best Practices - operational rules
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.