The Event Loop is not just a simple loop, but a sophisticated coordination mechanism between Call Stack, queues, and runtime. Essential for JS concurrency, it enables thousands of asynchronous operations without blocking the UI. This guide explores its structure, differences between Node.js and browsers, common bugs, advanced techniques, and 2025 trends like Structured Concurrency and scheduler.yield().

The event loop is the fundamental mechanism that allows JavaScript to execute non-blocking asynchronous operations. In 2025, with Node.js 22 and more sophisticated engines, understanding how the event loop works is essential for developers seeking real performance in modern applications.
If you develop with JavaScript — whether on the frontend with React, Vue, or Svelte, or on the backend with Node.js, Bun, or Deno — the event loop is present in every asynchronous operation you execute. Data fetching, timers, promises, DOM manipulation, stream processing: everything goes through this mechanism.
The critical point: most developers know the event loop exists, but do not understand how it actually works. And this superficial knowledge has a high cost in 2025.
With the popularization of real-time applications, WebSockets, Server-Sent Events, and serverless architectures, the event loop has become the most common bottleneck in high-performance applications. Understanding its nuances is no longer a "bonus skill" — it is an essential competency.
The event loop is the concurrency mechanism that allows JavaScript to execute non-blocking operations despite being, by nature, a single-threaded language. This apparent contradiction is resolved through an intelligent model of queues and execution cycles.
console.log('1. Início do script');setTimeout(() => { console.log('2. Timeout executado');}, 0);Promise.resolve().then(() => { console.log('3. Promise microtask');});console.log('4. Fim do script');// Output:// 1. Início do script// 4. Fim do script// 3. Promise microtask// 2. Timeout executadoThe event loop operates with multiple execution queues, each with distinct priority and behavior:
// Macrotask queuesetTimeout(() => console.log('Macrotask 1'), 0);setImmediate(() => console.log('Immediate 1')); // Node.js apenas// Microtask queuePromise.resolve().then(() => console.log('Microtask 1'));queueMicrotask(() => console.log('Microtask via queueMicrotask'));// process.nextTick (Node.js - executa antes das microtasks)process.nextTick(() => console.log('nextTick'));// Output em Node.js:// nextTick// Microtask 1// Microtask via queueMicrotask// Macrotask 1// Immediate 1In 2015, the event loop was relatively simple. V8 was in its 4.x version, and native Promise support was recent (ES6). Most developers still used callbacks, and async/await was a novelty on the horizon.
The basic model was:
Task Queue: where setTimeout, I/O callbacks, and UI events entered
Microtask Queue: introduced with Promises, but rarely used
With the standardization of async/await (ES2017) and the introduction of queueMicrotask in the specification, the behavior of microtasks became more predictable. V8 optimized Promise handling throughout its versions, reducing the overhead of object creation.
async function fetchData(urls) { const results = await Promise.all( urls.map(url => fetch(url).then(r => r.json())) ); return results;}In 2025, the ecosystem around the event loop has matured:
Mature worker threads: Real parallelism for CPU-bound tasks
Async context tracking: Asynchronous context tracking via AsyncLocalStorage
Bun and Deno: New runtimes with optimized implementations
scheduler.yield(): Native API to yield control to the event loop (Chrome 124+)
// AsyncLocalStorage — disponível desde Node.js 16, amplamente adotado em 2025import { AsyncLocalStorage } from 'async_hooks';const requestStorage = new AsyncLocalStorage();function handleRequest(req, res) { requestStorage.run({ requestId: generateId() }, () => { logRequest(req); processData(); });}async function processData() { const store = requestStorage.getStore(); console.log(`Processing request: ${store.requestId}`);}console.log('=== Início ===');setTimeout(() => console.log('1. setTimeout (macrotask)'), 0);Promise.resolve().then(() => console.log('2. Promise.then (microtask)'));queueMicrotask(() => console.log('3. queueMicrotask (microtask)'));if (typeof process !== 'undefined' && process.nextTick) { process.nextTick(() => console.log('4. nextTick (Node.js only)'));}console.log('=== Fim do script ===');// Output:// === Início ===// === Fim do script ===// 4. nextTick (se Node.js)// 2. Promise.then// 3. queueMicrotask// 1. setTimeoutThe order of execution directly affects:
Rendering guarantees: Browsers render between macrotasks
Event loop blocking: Long microtasks block everything
Order of state updates: React and other frameworks depend on this behavior
// PROBLEMA: loop pesado dentro de microtask bloqueia o event loop inteiroasync function processarDados() { const dados = await buscarDados(); for (let i = 0; i < 10000000; i++) { processarItem(dados[i]); }}// SOLUÇÃO: dividir em chunks cedendo o controle entre cada umasync function processarDadosOtimizado(dados) { for (let i = 0; i < dados.length; i += 1000) { const chunk = dados.slice(i, i + 1000); chunk.forEach(processarItem); await new Promise(resolve => setTimeout(resolve, 0)); }}The values below are references to guide decisions — run them in your own environment for accurate results.
import { performance } from 'perf_hooks';function benchmark(name, fn, iterations = 100000) { const start = performance.now(); for (let i = 0; i < iterations; i++) { fn(); } const end = performance.now(); console.log(`${name}: ${((end - start) / iterations * 1000).toFixed(3)}µs por operação`);}benchmark('Promise.then', () => { Promise.resolve().then(() => {});});benchmark('queueMicrotask', () => { queueMicrotask(() => {});});Promise.then / queueMicrotask
setTimeout(0)
Async function overhead
Fractions of a microsecond
1–5ms (minimum imposed by the runtime)
Similar to Promise.then
┌───────────────────────────┐│ Timers │ setTimeout, setInterval└─────────────┬─────────────┘ ▼┌───────────────────────────┐│ Pending callbacks │ erros de I/O da iteração anterior└─────────────┬─────────────┘ ▼┌───────────────────────────┐│ Poll │ aguarda I/O, executa callbacks└─────────────┬─────────────┘ ▼┌───────────────────────────┐│ Check │ setImmediate└─────────────┬─────────────┘ ▼┌───────────────────────────┐│ Close callbacks │ socket.on('close', ...)└───────────────────────────┘Entre CADA fase, microtasks são executadas.// No browser, o modelo é mais simples:// 1. Execute todo o JavaScript síncrono// 2. Execute todas as microtasks// 3. Render (se necessário)// 4. Execute próxima macrotask// requestAnimationFrame — executa antes do paintrequestAnimationFrame(() => { console.log('Antes do render');});// requestIdleCallback — para trabalho não urgenterequestIdleCallback(() => { console.log('Browser está ocioso');});// ❌ Assumir que são equivalentessetTimeout(() => console.log('timeout'), 0);setImmediate(() => console.log('immediate'));// A ordem pode variar em Node.js dependendo do contexto de I/O.// setImmediate não existe no browser — use setTimeout(0) lá.// ❌ Processa tudo de uma vezasync function importUsers(users) { for (const user of users) { await saveUser(user); }}// ✅ Processa em batches cedendo o controle entre cada umasync function importUsersBatched(users, batchSize = 100) { for (let i = 0; i < users.length; i += batchSize) { const batch = users.slice(i, i + batchSize); await Promise.all(batch.map(saveUser)); await new Promise(resolve => setTimeout(resolve, 0)); reportProgress(Math.min(i + batchSize, users.length), users.length); }}// ✅ Cancelamento limpo com AbortControllerasync function fetchWithCancel(url, signal) { const response = await fetch(url, { signal }); return response.json();}const controller = new AbortController();fetchWithCancel('/api/data', controller.signal) .then(data => console.log(data)) .catch(err => { if (err.name === 'AbortError') { console.log('Request cancelado'); } });setTimeout(() => controller.abort(), 5000);The event loop is not an implementation detail — it is the JavaScript execution model. Ignoring how it works means writing code with timing bugs waiting to appear in production.
✅ Microtasks for atomic state updates
✅ Chunks + yield for processing large volumes
✅ requestIdleCallback for non-urgent tasks in the browser
✅ AbortController for clean cancellation
✅ Worker threads for real CPU-bound work
MDN — Execution model
Node.js Docs — The Node.js Event Loop
V8 Blog — v8.dev/blog
HTML Spec — Event Loop Processing Model
Takeaway: "In JavaScript, time is not linear — it is a queue of tasks, microtasks, and callbacks. Whoever understands the queue, understands time."