How do you run async operations in parallel vs in sequence correctly?

3 minintermediatenodejsparallelsequencepromise-allconcurrency

Quick Answer

Sequential: await each step in a for...of loop when later steps depend on earlier ones. Parallel: start all operations first (or map to promises) and await Promise.all. Avoid awaiting inside array.forEach (it doesn't wait) and avoid awaiting independent tasks one-by-one.

Detailed Answer

Answer:

Sequential — when each step depends on the previous, or you must limit load:

const results = [];
for (const id of ids) {
  results.push(await fetchItem(id)); // one at a time
}

Parallel — when operations are independent, run them together:

// Kick them all off, then wait for all
const results = await Promise.all(ids.map(id => fetchItem(id)));

This can be dramatically faster: three 100ms calls take ~100ms in parallel vs ~300ms sequentially.

Common mistakes:

  1. Accidental sequencing — awaiting independent calls one by one:
// ❌ Slow: b doesn't need a
const a = await getA();
const b = await getB();
// ✅ Fast:
const [a, b] = await Promise.all([getA(), getB()]);
  1. forEach doesn't await:
// ❌ Fires all, but the function returns before they finish
items.forEach(async (i) => { await save(i); });
// ✅ Wait for completion
await Promise.all(items.map(i => save(i)));
// ✅ Or truly sequential:
for (const i of items) { await save(i); }
  1. Unbounded parallelismPromise.all over 10,000 items opens 10,000 connections at once. Use a concurrency limit (e.g., p-limit, or process in batches) to protect downstream services and the libuv thread pool.

Rule of thumb: default to parallel for independent work (Promise.all), sequential only when there's a real dependency or a need to throttle.