Node.js Event Loop, Async Patterns & Streams

The event loop phases, timers and microtasks, promises and async/await, callback -> promise -> async evolution, backpressure with streams, and avoiding event loop blocking.

IntermediateUpdated 2026-10-2954K views

Event Loop Phases

Each loop iteration (tick) goes through phases: timers (setTimeout/setInterval) -> pending callbacks -> idle, prepare -> poll (IO) -> check (setImmediate) -> close callbacks. Microtasks (promise continuations, process.nextTick) run after every phase, before moving on.

console.log('1');
setTimeout(() => console.log('2'), 0);        // timers
setImmediate(() => console.log('3'));         // check
Promise.resolve().then(() => console.log('4'));// 微任务
process.nextTick(() => console.log('5'));      // nextTick 优先级最高

// 输出:1 5 4 2 3(或 1 5 4 3 2,取决于 IO 阶段)

From Callbacks to async/await

// 回调地狱(旧写法)
getUser(1, (err, user) => {
  getOrders(user.id, (err, orders) => {
    // 越缩越深
  });
});

// Promise
getUser(1)
  .then(user => getOrders(user.id))
  .then(orders => console.log(orders))
  .catch(err => console.error(err));

// async/await:看起来像同步
async function main() {
  try {
    const user = await getUser(1);
    const orders = await getOrders(user.id);
    console.log(orders);
  } catch (err) {
    console.error(err);
  }
}
main();

// 并行执行
const [a, b] = await Promise.all([fetchA(), fetchB()]);
Sequential await in a loop is a common performance bug. Use Promise.all for independent async work, but beware it fires everything at once — for fixed concurrency use p-limit or manual chunking.

Streams and Backpressure

Streams process data in chunks instead of buffering everything — a 10GB file can be hashed or uploaded with constant memory. Backpressure propagates automatically: a slow consumer pauses the producer.

import { createReadStream, createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';

// 旧写法:read.pipe(transform).pipe(write)
// 推荐写法:pipeline 自动处理错误与背压
await pipeline(
  createReadStream('input.tar.gz'),
  zlib.createGunzip(),
  createWriteStream('output/')
);

// HTTP body 就是流
app.post('/upload', (req, res) => {
  req.pipe(fs.createWriteStream(dest));
});

Keeping the Loop Unblocked

  • Never run CPU-heavy sync code in request handlers — offload to worker_threads or a child process.
  • Avoid the sync fs API (fs.readFileSync) in servers; it pauses all requests.
  • JSON.parse of multi-MB payloads is expensive — validate payload size at the gateway.
  • Track lag with perf_hooks or --inspect; the 'UV_THREADPOOL_SIZE' env increases the 4-thread pool for IO-heavy fs/crypto calls.

Friends & Partners