π The Event Loop
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>How the event loop works</title>
</head>
<body>
<div class="el-info" id="elinfo">
<style>
.el-info {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft: #e3ebfd;
--line: #ddd6c9;
--display-font: 'Fraunces', Georgia, serif;
--body-font: 'Inter', system-ui, sans-serif;
--mono-font: 'IBM Plex Mono', monospace;
max-width: 880px;
margin: 0 auto;
box-sizing: border-box;
padding: 2rem;
font-family: var(--body-font);
color: var(--ink);
background: var(--paper);
}
.el-info[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1c212b;
--accent: #5b8def;
--accent-soft: #202942;
--line: #2c313d;
}
.el-info * { box-sizing: border-box; }
.el-eyebrow {
font-family: var(--mono-font); font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase;
color: var(--accent); text-align: center; margin: 1.5rem 0 0.3rem;
}
.el-title {
font-family: var(--display-font); font-size: clamp(1.5rem, 5vw, 2rem); text-align: center;
margin: 0 0 0.5rem; letter-spacing: -0.01em;
}
.el-sub { color: var(--ink-soft); font-size: 0.9rem; text-align: center; margin: 0 0 2rem; max-width: 440px; margin-left: auto; margin-right: auto; }
/* ---- diagram ---- */
.el-diagram { display: flex; align-items: center; justify-content: center; gap: 0.8rem; margin-bottom: 1.2rem; flex-wrap: wrap; }
.el-box { border: 1px solid var(--line); border-radius: 10px; background: var(--paper-alt); padding: 0.9rem; width: 190px; }
.el-box-label { display: flex; align-items: center; gap: 0.4rem; font-size: 0.68rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-soft); margin-bottom: 0.7rem; }
.el-box-label svg { width: 14px; height: 14px; color: var(--ink-soft); }
.el-queue-row { display: flex; gap: 0.4rem; min-height: 34px; }
.el-chip {
flex: 1; display: flex; align-items: center; justify-content: center; height: 34px;
border: 1px solid var(--line); border-radius: 6px; background: var(--paper);
font-family: var(--mono-font); font-size: 0.76rem; font-weight: 700; color: var(--ink-soft);
transition: opacity 0.25s, transform 0.25s;
}
.el-chip.is-gone { opacity: 0; transform: scale(0.7); }
.el-stack-slot {
height: 34px; border-radius: 6px; border: 1px dashed var(--line); background: var(--paper);
display: flex; align-items: center; justify-content: center;
font-family: var(--mono-font); font-size: 0.76rem; font-weight: 700; color: var(--line);
transition: border-color 0.25s, background 0.25s, color 0.25s;
}
.el-stack-slot.is-running { border: 1px solid var(--accent); background: var(--accent-soft); color: var(--accent); }
.el-loop-col { display: flex; flex-direction: column; align-items: center; gap: 0.3rem; }
.el-loop-icon {
width: 46px; height: 46px; border-radius: 50%; border: 1px solid var(--line); background: var(--paper-alt);
display: flex; align-items: center; justify-content: center; transition: border-color 0.25s, background 0.25s;
}
.el-loop-icon svg { width: 22px; height: 22px; color: var(--ink-soft); transition: color 0.25s; }
.el-loop-icon.is-checking { border-color: var(--accent); background: var(--accent-soft); }
.el-loop-icon.is-checking svg { color: var(--accent); animation: el-spin 0.6s linear infinite; }
@keyframes el-spin { to { transform: rotate(360deg); } }
.el-loop-caption { font-size: 0.66rem; color: var(--ink-soft); text-align: center; max-width: 90px; }
.el-status { text-align: center; font-size: 0.84rem; color: var(--ink-soft); min-height: 1.3em; margin-bottom: 1.5rem; }
.el-status b { color: var(--accent); }
.el-controls { display: flex; justify-content: center; margin-bottom: 0.5rem; }
.el-btn {
font-family: var(--mono-font); font-size: 0.82rem; font-weight: 600; padding: 0.6rem 1.2rem;
border-radius: 5px; border: 1px solid var(--accent); background: var(--accent); color: #fff; cursor: pointer;
}
.el-btn:disabled { opacity: 0.55; cursor: default; }
.el-hint { text-align: center; font-size: 0.78rem; color: var(--ink-soft); margin: 0.7rem 0 2rem; }
@media (prefers-reduced-motion: reduce) {
.el-info *, .el-info *::before, .el-info *::after { transition-duration: 0.001ms !important; animation-duration: 0.001ms !important; }
.el-loop-icon.is-checking svg { animation: none; }
}
</style>
<div class="el-diagram">
<div class="el-box">
<div class="el-box-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M12 11h4"/><path d="M12 16h4"/><path d="M8 11h.01"/><path d="M8 16h.01"/></svg>Callback queue</div>
<div class="el-queue-row" id="elQueue">
<div class="el-chip" data-c="0">cb1</div>
<div class="el-chip" data-c="1">cb2</div>
<div class="el-chip" data-c="2">cb3</div>
</div>
</div>
<div class="el-loop-col">
<div class="el-loop-icon" id="elLoopIcon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></svg></div>
<div class="el-loop-caption">Is the stack empty?</div>
</div>
<div class="el-box">
<div class="el-box-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"/><path d="M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12"/><path d="M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17"/></svg>Call stack</div>
<div class="el-stack-slot" id="elStackSlot">empty</div>
</div>
</div>
<p class="el-status" id="elStatus"> </p>
<div class="el-controls">
<button type="button" class="el-btn" id="elRunBtn">βΆ Start the loop</button>
</div>
<p class="el-hint">Only one callback ever runs at a time.</p>
</div>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@500;600&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<script>
(function () {
var root = document.getElementById('elinfo');
if (!root) return;
var bodyAttr = document.body.getAttribute('data-darkmode');
var dark;
if (bodyAttr === 'true') dark = true;
else if (bodyAttr === 'false') dark = false;
else {
var match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) root.setAttribute('data-theme', 'dark');
var runBtn = root.querySelector('#elRunBtn');
var chips = root.querySelectorAll('.el-chip');
var loopIcon = root.querySelector('#elLoopIcon');
var stackSlot = root.querySelector('#elStackSlot');
var status = root.querySelector('#elStatus');
var running = false;
function reset() {
chips.forEach(function (c) { c.classList.remove('is-gone'); });
loopIcon.classList.remove('is-checking');
stackSlot.classList.remove('is-running');
stackSlot.textContent = 'empty';
status.innerHTML = '\u00a0';
}
function processCallback(i, done) {
var chip = chips[i];
var label = chip.getAttribute('data-c') === '0' ? 'cb1' : (chip.getAttribute('data-c') === '1' ? 'cb2' : 'cb3');
loopIcon.classList.add('is-checking');
status.innerHTML = 'Stack empty? <b>checking\u2026</b>';
setTimeout(function () {
loopIcon.classList.remove('is-checking');
chip.classList.add('is-gone');
stackSlot.classList.add('is-running');
stackSlot.textContent = label;
status.innerHTML = 'Yes \u2014 running <b>' + label + '</b>';
}, 850);
setTimeout(function () {
stackSlot.classList.remove('is-running');
stackSlot.textContent = 'empty';
status.innerHTML = '\u00a0';
}, 1750);
setTimeout(done, 2100);
}
runBtn.addEventListener('click', function () {
if (running) return;
running = true;
runBtn.disabled = true;
reset();
var i = 0;
function next() {
if (i >= chips.length) {
loopIcon.classList.add('is-checking');
status.innerHTML = 'Queue empty \u2014 <b>it keeps checking anyway</b>';
setTimeout(function () {
loopIcon.classList.remove('is-checking');
running = false;
runBtn.disabled = false;
}, 2100);
return;
}
processCallback(i, function () { i++; next(); });
}
next();
});
reset();
})();
</script>
</body>
</html>π§© The Problem It Solves
You already know Node runs on a single thread. But a real app does dozens of things at once β handling requests, reading files, waiting on timers. How does one thread manage all of that without freezing?
The answer is the event loop. Think of it as a manager standing at a set of trays, constantly asking: "is anything ready for me to hand off to the one worker (the thread) I've got?" Nothing runs until the event loop decides it's that task's turn.
Two rules define what runs when:
Top-level code (anything not inside a callback) runs first, immediately, before the loop even starts.
Everything else β every callback β runs inside the event loop, once the loop decides it's time.
π’ Why Everything Is a Callback
Node is built around an event-triggered architecture. Instead of your code sitting and waiting for something to finish, you hand Node a callback and say "run this when the thing happens." Then Node moves on.
Things that "happen" and emit events:
π An HTTP request arrives
β° A timer expires
π A file finishes reading
The event loop's whole job is: π listen for these events β π₯ pick them up as they're ready β βΆοΈ run their callback. Heavy work gets quietly handed off to the thread pool along the way, so the main thread never gets stuck waiting.
ποΈ Inside the Loop: Four Phases, Not One Queue
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inside the event loop</title>
</head>
<body>
<div class="phases-info" id="phasesinfo">
<style>
.phases-info {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft: #e3ebfd;
--line: #ddd6c9;
--display-font: 'Fraunces', Georgia, serif;
--body-font: 'Inter', system-ui, sans-serif;
--mono-font: 'IBM Plex Mono', monospace;
max-width: 880px;
margin: 0 auto;
box-sizing: border-box;
padding: 2rem;
font-family: var(--body-font);
color: var(--ink);
background: var(--paper);
}
.phases-info[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1c212b;
--accent: #5b8def;
--accent-soft: #202942;
--line: #2c313d;
}
.phases-info * { box-sizing: border-box; }
.ph-eyebrow {
font-family: var(--mono-font); font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase;
color: var(--accent); text-align: center; margin: 1.5rem 0 0.3rem;
}
.ph-title {
font-family: var(--display-font); font-size: clamp(1.5rem, 5vw, 2rem); text-align: center;
margin: 0 0 0.5rem; letter-spacing: -0.01em;
}
.ph-sub { color: var(--ink-soft); font-size: 0.9rem; text-align: center; margin: 0 0 1.8rem; }
/* ---- circular diagram ---- */
.ph-circle-wrap { display: flex; justify-content: center; margin-bottom: 3rem; }
.ph-circle { position: relative; width: min(300px, 82vw); aspect-ratio: 1 / 1; }
.ph-circle > svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.ph-arc { fill: none; stroke: var(--line); stroke-width: 2.5; transition: stroke 0.25s; }
.ph-arc.is-live { stroke: var(--accent); }
.ph-center {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
text-align: center; width: 110px;
}
.ph-center-num {
font-family: var(--mono-font); font-size: 1.6rem; font-weight: 700; color: var(--line); transition: color 0.25s;
}
.ph-center-num.is-set { color: var(--accent); }
.ph-center-label {
font-size: 0.7rem; color: var(--ink-soft); margin-top: 0.1rem; min-height: 1.6em; line-height: 1.3;
}
.ph-node {
position: absolute; width: 0; height: 0;
}
.ph-node-badge {
position: absolute; left: 0; top: 0; transform: translate(-50%, -50%);
width: 58px; height: 58px; border-radius: 12px;
border: 1px solid var(--line); background: var(--paper);
display: flex; align-items: center; justify-content: center;
transition: border-color 0.25s, background 0.25s, transform 0.25s;
}
.ph-node-badge svg { display: block; width: 24px; height: 24px; color: var(--ink-soft); transition: color 0.25s; }
.ph-node.is-active .ph-node-badge { border-color: var(--accent); background: var(--accent-soft); transform: translate(-50%, -50%) scale(1.1); }
.ph-node.is-active .ph-node-badge svg { color: var(--accent); }
.ph-node-label {
position: absolute; left: 0; top: 40px; transform: translateX(-50%);
white-space: nowrap; font-size: 0.72rem; font-weight: 700; color: var(--ink-soft); text-align: center; transition: color 0.25s;
}
.ph-node.is-active .ph-node-label { color: var(--accent); }
.ph-node-tag {
position: absolute; left: 0; top: 58px; transform: translateX(-50%);
white-space: nowrap; font-size: 0.62rem; font-weight: 700; color: var(--accent); opacity: 0; transition: opacity 0.25s;
}
.ph-node.is-active .ph-node-tag { opacity: 1; }
.ph-controls { display: flex; justify-content: center; margin: 0.4rem 0 0.6rem; }
.ph-btn {
font-family: var(--mono-font); font-size: 0.82rem; font-weight: 600; padding: 0.6rem 1.2rem;
border-radius: 5px; border: 1px solid var(--accent); background: var(--accent); color: #fff; cursor: pointer;
}
.ph-btn:disabled { opacity: 0.55; cursor: default; }
.ph-hint { text-align: center; font-size: 0.78rem; color: var(--ink-soft); margin: 0.7rem 0 2rem; }
@media (prefers-reduced-motion: reduce) {
.phases-info *, .phases-info *::before, .phases-info *::after { transition-duration: 0.001ms !important; animation-duration: 0.001ms !important; }
}
</style>
<div class="ph-circle-wrap">
<div class="ph-circle">
<svg viewBox="0 0 240 240" aria-hidden="true">
<path class="ph-arc" data-arc="0" d="M120,20 A100,100 0 0,1 220,120"></path>
<path class="ph-arc" data-arc="1" d="M220,120 A100,100 0 0,1 120,220"></path>
<path class="ph-arc" data-arc="2" d="M120,220 A100,100 0 0,1 20,120"></path>
<path class="ph-arc" data-arc="3" d="M20,120 A100,100 0 0,1 120,20"></path>
</svg>
<div class="ph-center">
<div class="ph-center-num" id="phCenterNum">—</div>
<div class="ph-center-label" id="phCenterLabel"> </div>
</div>
<div class="ph-node" data-phase="0" style="left:50%; top:8.33%;">
<div class="ph-node-badge"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="10" x2="14" y1="2" y2="2"/><line x1="12" x2="15" y1="14" y2="11"/><circle cx="12" cy="14" r="8"/></svg></div>
<span class="ph-node-label">Timers</span>
</div>
<div class="ph-node" data-phase="1" style="left:91.67%; top:50%;">
<div class="ph-node-badge"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"/></svg></div>
<span class="ph-node-label">I/O polling</span>
<span class="ph-node-tag">the real work</span>
</div>
<div class="ph-node" data-phase="2" style="left:50%; top:91.67%;">
<div class="ph-node-badge"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/></svg></div>
<span class="ph-node-label">setImmediate()</span>
</div>
<div class="ph-node" data-phase="3" style="left:8.33%; top:50%;">
<div class="ph-node-badge"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m19 5 3-3"/><path d="m2 22 3-3"/><path d="M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z"/><path d="M7.5 13.5 10 11"/><path d="M10.5 16.5 13 14"/><path d="m12 6 6 6 2.3-2.3a2.4 2.4 0 0 0 0-3.4l-2.6-2.6a2.4 2.4 0 0 0-3.4 0Z"/></svg></div>
<span class="ph-node-label">Close callbacks</span>
</div>
</div>
</div>
<div class="ph-controls">
<button type="button" class="ph-btn" id="phRunBtn">βΆ Run one cycle</button>
</div>
<p class="ph-hint">Then it starts right back at Timers.</p>
</div>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@500;600&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<script>
(function () {
var root = document.getElementById('phasesinfo');
if (!root) return;
var bodyAttr = document.body.getAttribute('data-darkmode');
var dark;
if (bodyAttr === 'true') dark = true;
else if (bodyAttr === 'false') dark = false;
else {
var match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) root.setAttribute('data-theme', 'dark');
var runBtn = root.querySelector('#phRunBtn');
var nodes = root.querySelectorAll('.ph-node');
var arcs = root.querySelectorAll('.ph-arc');
var centerNum = root.querySelector('#phCenterNum');
var centerLabel = root.querySelector('#phCenterLabel');
var running = false;
var labels = ['Timers', 'I/O polling & callbacks', 'setImmediate()', 'Close callbacks'];
function reset() {
nodes.forEach(function (n) { n.classList.remove('is-active'); });
arcs.forEach(function (a) { a.classList.remove('is-live'); });
centerNum.textContent = '\u2014';
centerNum.classList.remove('is-set');
centerLabel.innerHTML = '\u00a0';
}
runBtn.addEventListener('click', function () {
if (running) return;
running = true;
runBtn.disabled = true;
reset();
var step = 750;
nodes.forEach(function (n, i) {
var t = i * step;
setTimeout(function () {
nodes.forEach(function (o) { o.classList.remove('is-active'); });
arcs.forEach(function (a) { a.classList.remove('is-live'); });
if (i > 0) arcs[i - 1].classList.add('is-live');
n.classList.add('is-active');
centerNum.textContent = String(i + 1);
centerNum.classList.add('is-set');
centerLabel.textContent = labels[i];
}, t);
});
var lastIndex = nodes.length - 1;
setTimeout(function () {
arcs[lastIndex].classList.add('is-live');
centerLabel.textContent = 'back to Timers\u2026';
}, lastIndex * step + 450);
setTimeout(function () {
reset();
running = false;
runBtn.disabled = false;
}, lastIndex * step + 1150);
});
reset();
})();
</script>
</body>
</html>It's tempting to imagine one long line of callbacks waiting their turn. That's wrong. The event loop actually cycles through four separate phases, each with its own queue β like four checkout lines, processed one at a time, in a fixed order. A phase fully empties its queue before the loop moves to the next.
Every cycle, in order: Timers β I/O β setImmediate β Close
# | Phase | Handles | Frequency in Real Apps |
|---|---|---|---|
1οΈβ£ | Expired Timers |
| Occasional |
2οΈβ£ | I/O Polling & Callbacks | Networking & file I/O | π₯ ~99% of app code |
3οΈβ£ |
| Runs right after I/O polling | Rare / advanced |
4οΈβ£ | Close Callbacks | Cleanup β server/socket shutdowns | Rare |
1οΈβ£ Expired Timers
What it does: fires callbacks whose timer has run out.
console.log('Start');
setTimeout(() => console.log('Timer callback (Phase 1)'), 0);
console.log('End');Start
End
Timer callback (Phase 1)Takeaway: even at 0ms, the callback waits its turn β top-level code always finishes first.
2οΈβ£ I/O Polling & Callbacks β where the real work happens
What it does: handles networking and file I/O. This is the phase where ~99% of real Node app code executes, since most apps are built around serving requests and touching files.
const fs = require('fs');
console.log('Start');
fs.readFile('file.txt', () => console.log('File read callback (Phase 2)'));
console.log('End');Start
End
File read callback (Phase 2)Takeaway: the read happens in the background (often on the thread pool); the callback only fires once the loop reaches this phase.
3οΈβ£ setImmediate()
What it does: a special timer designed to run right after I/O polling finishes β "as soon as I/O is done, before anything else."
fs.readFile('file.txt', () => {
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
});setImmediate
setTimeoutTakeaway: called inside an I/O callback, setImmediate() always beats setTimeout(0) β the loop moves straight from I/O into the setImmediate phase, while the timer phase is a whole lap away.
4οΈβ£ Close Callbacks
What it does: cleanup work β e.g. a server or socket shutting down.
const server = require('net').createServer();
server.on('close', () => console.log('Server closed (Phase 4)'));
server.listen(3000, () => server.close());Server closed (Phase 4)Takeaway: rarely touched in everyday app code, but this is exactly when 'close' handlers fire.
π Seeing All Four Phases in One Run
console.log('1: Top-level code');
setTimeout(() => console.log('2: Timer phase'), 0);
fs.readFile(__filename, () => {
console.log('3: I/O phase');
setImmediate(() => console.log('4: setImmediate phase'));
});
console.log('5: Top-level code (end)');1: Top-level code
5: Top-level code (end)
2: Timer phase
3: I/O phase
4: setImmediate phasePattern to notice: all top-level code runs first, then the loop works through Timers β I/O β setImmediate β Close, one fully-drained queue at a time.
β‘ The Two Queues That Cut in Line
There are two extra queues that don't play by the four-phase order β they jump in immediately after whichever phase just finished, before the loop moves to the next phase:
π
process.nextTick()β highest priority, runs right after the current phaseπ§© Microtasks (mainly resolved Promises) β same idea, runs right after the current phase
Example: if a Promise resolves while a timer callback is executing, the Promise's callback runs immediately after that timer callback finishes β it doesn't wait for I/O, setImmediate, or Close to happen first.
Function | Runs When |
|---|---|
π | Right after the current phase β highest priority |
β‘ | Specifically after the I/O phase |
These are advanced, edge-case tools β worth recognizing, rarely needed directly.
β±οΈ One Lap = One "Tick" β and When the App Exits
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>One lap = one tick</title>
</head>
<body>
<div class="tick-info" id="tickinfo">
<style>
.tick-info {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft: #e3ebfd;
--line: #ddd6c9;
--err: #dc2626;
--err-soft: #fbe4e2;
--display-font: 'Fraunces', Georgia, serif;
--body-font: 'Inter', system-ui, sans-serif;
--mono-font: 'IBM Plex Mono', monospace;
max-width: 880px;
margin: 0 auto;
box-sizing: border-box;
padding: 2rem;
font-family: var(--body-font);
color: var(--ink);
background: var(--paper);
}
.tick-info[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1c212b;
--accent: #5b8def;
--accent-soft: #202942;
--line: #2c313d;
--err: #f87171;
--err-soft: #3a1f1f;
}
.tick-info * { box-sizing: border-box; }
.tick-eyebrow {
font-family: var(--mono-font); font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase;
color: var(--accent); text-align: center; margin: 1.5rem 0 0.3rem;
}
.tick-title {
font-family: var(--display-font); font-size: clamp(1.4rem, 5vw, 1.9rem); text-align: center;
margin: 0 0 0.5rem; letter-spacing: -0.01em;
}
.tick-sub { color: var(--ink-soft); font-size: 0.9rem; text-align: center; margin: 0 0 1.8rem; max-width: 460px; margin-left: auto; margin-right: auto; }
/* ---- scenario toggle ---- */
.tick-toggle { display: flex; justify-content: center; gap: 0.5rem; margin-bottom: 1.6rem; flex-wrap: wrap; }
.tick-toggle-btn {
display: flex; align-items: center; gap: 0.4rem;
font-family: var(--body-font); font-size: 0.82rem; font-weight: 600; padding: 0.5rem 0.9rem;
border-radius: 20px; border: 1px solid var(--line); background: var(--paper); color: var(--ink-soft); cursor: pointer;
}
.tick-toggle-btn svg { width: 15px; height: 15px; }
.tick-toggle-btn.is-selected { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
/* ---- diagram ---- */
.tick-diagram { display: flex; align-items: center; justify-content: center; gap: 0.5rem; margin-bottom: 1.3rem; flex-wrap: wrap; }
.tick-node { display: flex; flex-direction: column; align-items: center; gap: 0.4rem; width: 96px; }
.tick-node-badge {
width: 54px; height: 54px; border-radius: 10px; display: flex; align-items: center; justify-content: center;
border: 1px solid var(--line); background: var(--paper-alt); transition: border-color 0.25s, background 0.25s, transform 0.25s;
}
.tick-node-badge svg { width: 24px; height: 24px; color: var(--ink-soft); transition: color 0.25s; }
.tick-node.is-active .tick-node-badge { border-color: var(--accent); background: var(--accent-soft); transform: scale(1.08); }
.tick-node.is-active .tick-node-badge svg { color: var(--accent); }
.tick-node.is-exit .tick-node-badge { border-radius: 50%; }
.tick-node.is-exit.is-active .tick-node-badge { border-color: var(--err); background: var(--err-soft); }
.tick-node.is-exit.is-active .tick-node-badge svg { color: var(--err); }
.tick-node-label { font-size: 0.72rem; font-weight: 700; text-align: center; color: var(--ink-soft); }
.tick-node.is-active .tick-node-label { color: var(--accent); }
.tick-node.is-exit.is-active .tick-node-label { color: var(--err); }
.tick-diamond {
width: 66px; height: 66px; flex-shrink: 0; border: 1px solid var(--line); background: var(--paper-alt);
transform: rotate(45deg); border-radius: 8px; display: flex; align-items: center; justify-content: center;
transition: border-color 0.25s, background 0.25s;
}
.tick-diamond-inner { transform: rotate(-45deg); font-size: 0.6rem; font-weight: 700; text-align: center; color: var(--ink-soft); line-height: 1.15; transition: color 0.25s; }
.tick-node.is-active .tick-diamond { border-color: var(--accent); background: var(--accent-soft); }
.tick-node.is-active .tick-diamond-inner { color: var(--accent); }
.tick-arrow { color: var(--line); font-size: 1.1rem; flex-shrink: 0; }
/* ---- status ---- */
.tick-status-row { display: flex; align-items: center; justify-content: center; gap: 0.6rem; margin-bottom: 0.5rem; }
.tick-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--line); transition: background 0.3s, box-shadow 0.3s; }
.tick-dot.is-alive { background: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft); }
.tick-dot.is-exited { background: var(--err); box-shadow: 0 0 0 4px var(--err-soft); }
.tick-status-text { font-family: var(--mono-font); font-size: 0.8rem; color: var(--ink-soft); }
.tick-status-text b { color: var(--ink); }
.tick-caption { text-align: center; font-size: 0.84rem; color: var(--ink-soft); min-height: 1.3em; margin-bottom: 1.3rem; }
.tick-caption b { color: var(--accent); }
.tick-controls { display: flex; justify-content: center; margin-bottom: 0.5rem; }
.tick-btn {
font-family: var(--mono-font); font-size: 0.82rem; font-weight: 600; padding: 0.6rem 1.2rem;
border-radius: 5px; border: 1px solid var(--accent); background: var(--accent); color: #fff; cursor: pointer;
}
.tick-btn:disabled { opacity: 0.55; cursor: default; }
.tick-hint { text-align: center; font-size: 0.78rem; color: var(--ink-soft); margin: 0.7rem 0 2rem; }
@media (prefers-reduced-motion: reduce) {
.tick-info *, .tick-info *::before, .tick-info *::after { transition-duration: 0.001ms !important; animation-duration: 0.001ms !important; }
}
</style>
<div class="tick-toggle">
<button type="button" class="tick-toggle-btn is-selected" data-scenario="file">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/></svg>
Read a file
</button>
<button type="button" class="tick-toggle-btn" data-scenario="server">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>
Listen for requests
</button>
</div>
<div class="tick-diagram">
<div class="tick-node" id="tickNodeTick">
<div class="tick-node-badge"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></svg></div>
<span class="tick-node-label" id="tickLabel">Tick</span>
</div>
<span class="tick-arrow">β</span>
<div class="tick-node" id="tickNodeDecision">
<div class="tick-diamond"><div class="tick-diamond-inner">Pending<br>work?</div></div>
<span class="tick-node-label"> </span>
</div>
<span class="tick-arrow">β</span>
<div class="tick-node is-exit" id="tickNodeExit">
<div class="tick-node-badge"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.77.04"/></svg></div>
<span class="tick-node-label">Exit</span>
</div>
</div>
<div class="tick-status-row">
<div class="tick-dot" id="tickDot"></div>
<span class="tick-status-text" id="tickStatusText">Not started</span>
</div>
<p class="tick-caption" id="tickCaption"> </p>
<div class="tick-controls">
<button type="button" class="tick-btn" id="tickRunBtn">βΆ Run</button>
</div>
<p class="tick-hint">Same loop, same question β different answer each time.</p>
</div>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@500;600&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<script>
(function () {
var root = document.getElementById('tickinfo');
if (!root) return;
var bodyAttr = document.body.getAttribute('data-darkmode');
var dark;
if (bodyAttr === 'true') dark = true;
else if (bodyAttr === 'false') dark = false;
else {
var match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) root.setAttribute('data-theme', 'dark');
var toggleBtns = root.querySelectorAll('.tick-toggle-btn');
var runBtn = root.querySelector('#tickRunBtn');
var nodeTick = root.querySelector('#tickNodeTick');
var nodeDecision = root.querySelector('#tickNodeDecision');
var nodeExit = root.querySelector('#tickNodeExit');
var tickLabel = root.querySelector('#tickLabel');
var dot = root.querySelector('#tickDot');
var statusText = root.querySelector('#tickStatusText');
var caption = root.querySelector('#tickCaption');
var scenario = 'file';
var running = false;
toggleBtns.forEach(function (btn) {
btn.addEventListener('click', function () {
if (running) return;
scenario = btn.getAttribute('data-scenario');
toggleBtns.forEach(function (b) { b.classList.toggle('is-selected', b === btn); });
resetVisual();
});
});
function resetVisual() {
nodeTick.classList.remove('is-active');
nodeDecision.classList.remove('is-active');
nodeExit.classList.remove('is-active');
tickLabel.textContent = 'Tick';
dot.className = 'tick-dot';
statusText.textContent = 'Not started';
caption.innerHTML = '\u00a0';
}
function runTick(n, exitsAfter, done) {
tickLabel.textContent = 'Tick ' + n;
nodeDecision.classList.remove('is-active');
nodeExit.classList.remove('is-active');
nodeTick.classList.add('is-active');
dot.className = 'tick-dot is-alive';
statusText.innerHTML = '<b>Tick ' + n + '</b> running\u2026';
caption.innerHTML = '\u00a0';
setTimeout(function () {
nodeTick.classList.remove('is-active');
nodeDecision.classList.add('is-active');
var pending = exitsAfter ? (n < exitsAfter) : true;
statusText.innerHTML = 'Pending work? <b>' + (pending ? 'Yes' : 'No') + '</b>';
caption.innerHTML = pending
? (scenario === 'server' ? 'Still listening for requests \u2014 <b>keep going</b>' : 'File still reading \u2014 <b>keep going</b>')
: 'Nothing left to wait on \u2014 <b>exit</b>';
setTimeout(function () {
if (pending) {
nodeDecision.classList.remove('is-active');
done(true);
} else {
nodeExit.classList.add('is-active');
dot.className = 'tick-dot is-exited';
statusText.innerHTML = '<b>Process exited</b>';
done(false);
}
}, 750);
}, 650);
}
runBtn.addEventListener('click', function () {
if (running) return;
running = true;
runBtn.disabled = true;
toggleBtns.forEach(function (b) { b.disabled = true; });
resetVisual();
var exitsAfter = scenario === 'file' ? 2 : null;
var maxTicks = scenario === 'file' ? 2 : 4;
var n = 1;
function step() {
if (n > maxTicks) {
if (scenario === 'server') {
caption.innerHTML = 'This never stops on its own \u2014 <b>the server stays up</b>';
}
running = false;
runBtn.disabled = false;
toggleBtns.forEach(function (b) { b.disabled = false; });
return;
}
runTick(n, exitsAfter, function (continues) {
n++;
if (continues) { setTimeout(step, 150); }
else {
running = false;
runBtn.disabled = false;
toggleBtns.forEach(function (b) { b.disabled = false; });
}
});
}
step();
});
resetVisual();
})();
</script>
</body>
</html>A full pass through all four phases (plus the priority queues) is called a tick. After each tick, Node asks one question: is there still pending work?
β No pending timers or I/O β the app exits
β Something's still pending β the loop runs another tick
This is exactly why a server stays alive after starting: listening for HTTP requests is an ongoing I/O task, so there's always something pending, so the loop never stops. Same logic applies to a background file read β the app stays up until it resolves.
π Why Any of This Matters
Node has to serve potentially millions of users through a single thread, and the event loop is the mechanism that makes that possible β by rapidly cycling through phases and always finding the next bit of ready work instead of sitting idle.
β Upside | β οΈ Trade-off |
|---|---|
Lightweight, scalable β no new thread per user | Block the thread once, and every user feels it |
For contrast: PHP/Apache spins up a new thread per user β heavier on resources, but one slow request can't take down anyone else's. Node chooses efficiency and puts the burden on you to not block the thread.
β Your Job: Don't Block the Loop
Since blocking hurts everyone, follow these:
π« Never use sync versions of
fs,crypto,zlibinside callbacks β sync calls are fine only in top-level code, which runs before the loop even startsπ§ Avoid heavy calculations directly inside callback code
π¦ Watch out for huge JSON β
JSON.parse/stringifyslow down at scaleπ Avoid complex regex (nested quantifiers, backreferences)
For genuinely heavy work later on: offload manually to the π§΅ thread pool or use πΆ child processes.
π Key Takeaways
π Concept | β Explanation |
|---|---|
Event loop | Runs all callback code β Node's "heart" |
Event-triggered | Events (requests, timers, file reads) trigger callbacks |
4 phases | Timers β I/O β |
I/O phase | Where ~99% of app code runs |
| Jump the queue β run right after current phase |
Tick | One full loop cycle |
Continue/exit | Loop continues if timers/I/O are pending |
Why it matters | Enables async, non-blocking code on a single thread |
Blocking risk | Freezes the app for all users at once |
Best practices | Avoid sync fs/crypto/zlib in callbacks, heavy compute, huge JSON, complex regex |