Sync vs Async Performance

Chunking CPU Work with setImmediate Keeps the Event Loop Breathing

Sync vs Async Performance

Synchronous work blocks the event loop no matter how many Promises you wrap it in — learn when async genuinely helps, when it only adds overhead, and how to chunk large algorithms with setImmediate.

5 min read Level 3/5 #dsa#nodejs#interview
What you'll learn
  • Explain why wrapping sync work in a Promise does not unblock the event loop
  • Chunk a long-running sort into cooperative pieces using setImmediate
  • Know the scenarios where true async (I/O, worker threads) actually helps latency

Wrapping a slow algorithm in async function or Promise does not make it non-blocking. Async syntax only changes when the function is called relative to the microtask queue — the CPU work inside still runs synchronously on the same thread. This lesson draws a clear line between async that helps and async that is just decoration.

The Promise Wrapper Myth

// This does NOT unblock the event loop — the loop is stuck inside the Promise
async function slowSort(arr) {
  arr.sort((a, b) => a - b); // still synchronous
  return arr;
}

await slowSort(hugeArray); // await just defers calling, CPU is still hogged

The await before the call yields to the microtask queue for one tick, then immediately resumes. All the CPU time for the sort runs in that resumed microtask — the poll phase never gets a look in.

When Async Actually Helps

Async is a genuine win only when the work is off-thread:

SituationBlocking?Fix
fs.readFileSyncYesUse fs.promises.readFile
Database query (sync driver)YesUse async driver
CPU-bound sort in main threadYesWorker thread or chunking
fetch / network callNo (awaits kernel)Already non-blocking
setTimeout(fn, 0)NoYields to timer phase

Chunking with setImmediate

setImmediate fires in the check phase, after the poll phase has had a chance to grab waiting I/O. Yielding to it between chunks of work lets the loop breathe.

/**
 * Process a large array in chunks of `size` items.
 * Between each chunk the event loop can service pending I/O.
 */
function processInChunks(items, size, processFn) {
  return new Promise((resolve) => {
    let i = 0;

    function next() {
      const end = Math.min(i + size, items.length);
      while (i < end) {
        processFn(items[i]);
        i++;
      }
      if (i < items.length) {
        setImmediate(next); // yield to the event loop, then continue
      } else {
        resolve();
      }
    }

    next();
  });
}

// Example: sum 2 million numbers without blocking
const data = Array.from({ length: 2_000_000 }, (_, k) => k);
let total = 0;
await processInChunks(data, 50_000, (n) => { total += n; });
console.log(total);

Chunk size is a tuning knob: larger chunks mean less overhead but longer individual blocks; smaller chunks keep latency low but add more setImmediate round-trips.

When Chunking Adds Overhead Without Benefit

Chunking is not free — each setImmediate call has a small cost. Avoid it when the work is already fast:

  • Sorting 1 000 items takes under 1 ms; no chunking needed.
  • A single pass over a 10-item array is negligible.
  • Internal batch jobs with no latency requirement can run sync.

Reserve chunking (or worker threads, covered next) for operations that measurably block the loop under real load.

Up Next

When chunking still is not fast enough, move CPU-bound work entirely off the main thread with worker threads.

Worker Threads for CPU-Bound Algorithms →