Engineering Notes
Home/Languages/JavaScript/Parallel vs Sequential Async Operations
Languages · JavaScript

Parallel vs Sequential Async Operations

Each await blocks the function (not the whole program — see the Async/Await topic) from proceeding until that specific operation finishes, before even starting the next one.

💻 Languages● Advanced⏱ 6 min read🛠️ 3 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • Sequential: Each Step Waits for the Previous One
  • Parallel: All Operations Start Immediately
  • The Bug Pattern: Accidental Serialization
  • When Sequential Is Actually Required
  • Partial Parallelism — Mixing Both

1Sequential: Each Step Waits for the Previous One

js
async function sequential() {
  const a = await delay(100, "a"); // starts, waits 100ms
  const b = await delay(100, "b"); // does NOT start until `a` is done — another 100ms
  const c = await delay(100, "c"); // does NOT start until `b` is done — another 100ms
  return [a, b, c];
}
// total time: ~300ms

Each await blocks the function (not the whole program — see the Async/Await topic) from proceeding until that specific operation finishes, before even starting the next one.

2Parallel: All Operations Start Immediately

js
async function parallel() {
  const promiseA = delay(100, "a"); // starts immediately, NOT awaited yet
  const promiseB = delay(100, "b"); // starts immediately too — overlaps with A
  const promiseC = delay(100, "c"); // starts immediately too — overlaps with A and B

  const [a, b, c] = await Promise.all([promiseA, promiseB, promiseC]); // wait for all THREE at once
  return [a, b, c];
}
// total time: ~100ms — all three ran concurrently

The critical detail: calling delay(100, "a") starts the operation immediately, whether or not you await it right away. Sequential code accidentally forces this waiting by awaiting each call before starting the next.

3The Bug Pattern: Accidental Serialization

js
// LOOKS like it should be fine, but is needlessly sequential:
async function loadAllUsersSlow(ids) {
  const users = [];
  for (const id of ids) {
    const user = await fetchUser(id); // each iteration waits for the PREVIOUS one
    users.push(user);
  }
  return users;
}

// Fixed — starts all requests immediately, waits for all together:
async function loadAllUsersFast(ids) {
  const promises = ids.map(id => fetchUser(id)); // all fetches START here, immediately
  return Promise.all(promises);                    // NOW wait for all of them together
}

loadAllUsersSlow with 5 users taking 100ms each takes ~500ms total. loadAllUsersFast takes ~100ms total (limited by the slowest single request, not the sum of all of them).

Sequential bars laid end to end sum to 300ms; parallel bars stacked at the same start point finish in 100ms
Sequential bars laid end to end sum to 300ms; parallel bars stacked at the same start point finish in 100ms

4When Sequential Is Actually Required

Sequential execution is necessary — not just accidental — when a later step genuinely depends on an earlier step's result:

js
async function checkoutFlow() {
  const cart = await getCart();                 // step 2 needs cart's contents
  const total = await calculateTotal(cart);       // step 3 needs the calculated total
  const receipt = await chargeCard(total);         // each step's INPUT comes from the previous OUTPUT
  return receipt;
}

Also required when operations must not overlap for correctness (writing to the same file, respecting a strict rate limit, maintaining a specific order of side effects).

5Partial Parallelism — Mixing Both

js
async function loadPage(userId) {
  // These two are INDEPENDENT of each other — run them in parallel:
  const [user, settings] = await Promise.all([
    fetchUser(userId),
    fetchSettings(userId),
  ]);

  // This DEPENDS on `user` — must run sequentially after the above:
  const recommendations = await fetchRecommendations(user.preferences);

  return { user, settings, recommendations };
}

Identify genuine dependencies first; run everything else concurrently.

6Controlled Concurrency (Not Fully Parallel, Not Fully Sequential)

Running 10,000 requests all at once via Promise.all can overwhelm a server or hit rate limits. A common middle ground processes a limited batch size at a time:

js
async function processInBatches(items, batchSize, asyncFn) {
  const results = [];
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    const batchResults = await Promise.all(batch.map(asyncFn)); // parallel WITHIN a batch
    results.push(...batchResults);
    // sequential BETWEEN batches — waits for one batch before starting the next
  }
  return results;
}
Under the HoodHow Promise.all Actually Tracks Completion▾

Promise.all isn't implemented as "poll every promise until they're all done" — the spec algorithm (PerformPromiseAll) sets up a real counter and a fixed-size results array up front, then lets each individual promise's own resolution mechanism (the [[PromiseFulfillReactions]] machinery from the Promises topic) do the actual work of reporting back.

The algorithm, precisely

  1. Allocate a results array sized to the input list, and a counter initialized to the number of promises still remaining.
  2. For each input promise at index i, attach a .then() reaction that, when it fires, writes the resolved value into results[i] and decrements the counter.
  3. When the counter reaches zero, resolve Promise.all's own returned promise with the completed results array.
  4. Separately, attach a rejection reaction to every input promise that, on first firing, immediately rejects Promise.all's returned promise with that reason — and does nothing on any subsequent rejection (the returned promise can only settle once, per the Promises topic's single-settlement guard).
text
promises: [P0, P1, P2]   counter starts at 3
P1 resolves first  → results[1] = value, counter → 2
P0 resolves second  → results[0] = value, counter → 1
P2 resolves third    → results[2] = value, counter → 0  → Promise.all's promise resolves with results

Why results preserve input order despite resolving out of order

Section 1's examples show Promise.all([a, b, c]) giving back [a, b, c] in order, even though b might genuinely finish first. This is why: each reaction writes to a fixed index (results[i], based on the promise's position in the input array, not the order it resolves in) — order preservation isn't a sorting step at the end, it's baked into which array slot each reaction targets from the start.

Why one rejection is final even though other promises keep running

The other, still-pending promises in the array are not cancelled when one rejects — nothing in JavaScript's Promise model supports cancelling a promise (that's precisely the gap the Abort Controller topic's AbortController exists to fill at the operation level, not the Promise level). They continue running to completion in the background; their eventual results are simply never read, because Promise.all's own promise already settled and its single-settlement guard makes any further resolve/reject attempts on it a no-op.

⚠️ Common Pitfalls

  1. Using await inside a for loop when operations are independent — the single most common accidental-serialization bug; fix with .map() + Promise.all.
  2. Assuming Promise.all limits concurrency — it doesn't; it starts everything at once, which can overwhelm a downstream service with truly large arrays (see section 6 for the fix).
  3. Forcing sequential execution "to be safe" when there's no real dependency — needlessly slows things down; only serialize when a genuine ordering or dependency requirement exists.
  4. Missing a real dependency and parallelizing anyway — running chargeCard() before calculateTotal() finishes would be a correctness bug, not just a performance one.

📋 Quick Reference

text
for (x of items) { await f(x) }         → sequential, ~sum of all durations
await Promise.all(items.map(f))          → parallel, ~duration of the slowest one
independent steps → parallelize (Promise.all)
dependent steps (B needs A's result)      → must stay sequential

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1How do you run async operations sequentially vs in parallel?core▾

Sequential: for (const x of items) await work(x). Parallel: await Promise.all(items.map(work)). Bounded concurrency: a pool/limiter running N at a time.

Follow-ups interviewers ask
  • Which is fastest? Parallel, if the downstream can handle it.
Q2Why limit concurrency?core▾

Unbounded parallelism can overload APIs (rate limits), databases (connection pools), memory and sockets. A limit like 5–20 concurrent requests balances speed and safety.

Follow-ups interviewers ask
  • Implement a simple limiter? Keep a queue and start the next task when one finishes.
Q3Is Promise.all truly parallel?core▾

It runs I/O concurrently on one thread; the JavaScript callbacks still execute one at a time. CPU-bound work isn't parallelized.

Follow-ups interviewers ask
  • True parallelism? Workers.
Q4How do you process results as they complete?senior▾

Attach handlers to each promise, or use an async iterator/Promise.race loop to yield results in completion order; streams of results improve perceived performance.

Follow-ups interviewers ask
  • Ordering concern? Results arrive out of input order.
Q5When must operations be sequential?core▾

When each depends on the previous result, when order of side effects matters (migrations), or when rate limits require pacing.

Follow-ups interviewers ask
  • Compromise? Batches processed sequentially, items within a batch in parallel.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.