JavaScript Event Loop: The Heart of Non-Blocking Concurrency — Revisited in 2025Code & Development

Code & Development · 26 Jul 2026

JavaScript Event Loop: The Heart of Non-Blocking Concurrency — Revisited in 2025

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().

Genildo Souza26 Jul 2026Read: 6 min

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.

Why the Event Loop Still Matters in 2025?

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.

What Is the Event Loop: Fundamentals

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.

javascript
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 executado

The Queue Architecture

The event loop operates with multiple execution queues, each with distinct priority and behavior:

javascript
// Macrotask queue
setTimeout(() => console.log('Macrotask 1'), 0);
setImmediate(() => console.log('Immediate 1')); // Node.js apenas

// Microtask queue
Promise.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 1

Evolution of the Event Loop: From 2015 to 2025

The Landscape in 2015

In 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

The Revolution of 2017-2019

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.

javascript
async function fetchData(urls) {
  const results = await Promise.all(
    urls.map(url => fetch(url).then(r => r.json()))
  );
  return results;
}

The State of the Art in 2025

In 2025, the ecosystem around the event loop has matured:

  1. Mature worker threads: Real parallelism for CPU-bound tasks

  2. Async context tracking: Asynchronous context tracking via AsyncLocalStorage

  3. Bun and Deno: New runtimes with optimized implementations

  4. scheduler.yield(): Native API to yield control to the event loop (Chrome 124+)

javascript
// AsyncLocalStorage — disponível desde Node.js 16, amplamente adotado em 2025
import { 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}`);
}

Microtasks vs Macrotasks: The Conflict of Priorities

javascript
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. setTimeout

Why Does This Matter?

The 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

javascript
// PROBLEMA: loop pesado dentro de microtask bloqueia o event loop inteiro
async 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 um
async 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));
  }
}

Benchmarks: Measuring the Event Loop

The values below are references to guide decisions — run them in your own environment for accurate results.

javascript
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(() => {});
});

Operation

Promise.then / queueMicrotask

setTimeout(0)

Async function overhead

Order of magnitude

Fractions of a microsecond

1–5ms (minimum imposed by the runtime)

Similar to Promise.then

The practical difference: setTimeout(0) is orders of magnitude slower than microtasks for internal scheduling. Never use setTimeout(0) to coordinate state updates.

Node.js vs Browser: Divergent Event Loops

Node.js Event Loop (libuv)

Code
┌───────────────────────────┐
│  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.

Browser Event Loop

javascript
// 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 paint
requestAnimationFrame(() => {
  console.log('Antes do render');
});

// requestIdleCallback — para trabalho não urgente
requestIdleCallback(() => {
  console.log('Browser está ocioso');
});

Anti-Patterns and Best Practices in 2025

Error #1: Confusing setTimeout(0) with setImmediate

javascript
// ❌ Assumir que são equivalentes
setTimeout(() => 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á.

Error #2: Blocking the event loop with large volumes

javascript
// ❌ Processa tudo de uma vez
async function importUsers(users) {
  for (const user of users) {
    await saveUser(user);
  }
}

// ✅ Processa em batches cedendo o controle entre cada um
async 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);
  }
}

Error #3: Not using AbortController for cancellation

javascript
// ✅ Cancelamento limpo com AbortController
async 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);

Conclusion

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

🔗 References

  1. MDN — Execution model

  2. Node.js Docs — The Node.js Event Loop

  3. V8 Blog — v8.dev/blog

  4. 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."