The event loop in one sentence
Node.js runs JavaScript on a single thread. The event loop is the mechanism that allows it to handle concurrent I/O operations without blocking: when an async operation completes, its callback is queued. The event loop processes these callbacks in order.
The six phases
The event loop cycles through six phases. Each phase has a queue of callbacks to execute before moving to the next:
- timers —
setTimeoutandsetIntervalcallbacks whose delay has passed - pending callbacks — I/O error callbacks deferred from the previous cycle
- idle, prepare — Internal use only
- poll — Retrieve new I/O events, execute I/O callbacks. The event loop blocks here when idle.
- check —
setImmediatecallbacks - close callbacks — Cleanup callbacks like
socket.on('close')
Microtasks run between phases
Promise callbacks (.then, async/await) and queueMicrotask are microtasks. They run after each phase completes and before the next phase begins. This means a loop that continuously creates resolved promises can starve the event loop.
// Microtask queue drains completely before moving to next phase
Promise.resolve().then(() => console.log('microtask 1'));
setImmediate(() => console.log('check phase'));
Promise.resolve().then(() => console.log('microtask 2'));
// Output: microtask 1, microtask 2, check phasesetTimeout(0) vs setImmediate
// When called at the top level, order is non-deterministic:
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// When called inside an I/O callback, setImmediate always wins:
fs.readFile('file.txt', () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate')); // Always first
});Blocking the event loop
Any synchronous code that runs for more than a few milliseconds blocks every other request from being served during that time. Common culprits:
// ✗ Blocks the event loop for duration of computation
app.get('/parse', (req, res) => {
const result = JSON.parse(hugeJsonString); // 200ms on large payload
res.json(result);
});
// ✓ Move to a worker thread
import { Worker } from 'worker_threads';
app.get('/parse', async (req, res) => {
const result = await runInWorker('./json-parser-worker.js', hugeJsonString);
res.json(result);
});Measuring event loop lag
// Measure event loop delay
import { monitorEventLoopDelay } from 'perf_hooks';
const histogram = monitorEventLoopDelay({ resolution: 10 });
histogram.enable();
setInterval(() => {
log.info({
mean: histogram.mean / 1e6, // Convert nanoseconds to ms
p99: histogram.percentile(99) / 1e6,
}, 'Event loop delay');
histogram.reset();
}, 10000);A healthy event loop has p99 delay under 10ms. Over 100ms and you have a blocking operation causing user-visible latency.