Promise Concurrency Pool

Hard100% Free~30 mins#promise-pool#concurrency#async-await#promise-race#rate-limiting#asynchronous
Key Learning Objectives
✓

Differentiate unbounded execution (Promise.all) from bounded concurrency (Promise Pool / Rate Limiter).

✓

Implement promisePool(tasks, limit) to enforce that at most K asynchronous tasks run concurrently.

✓

Guarantee that output results maintain original task input ordering regardless of individual completion times.

✓

Use Promise.race on an active executing set to dynamically trigger subsequent tasks as soon as any active task resolves.

✓

Handle edge cases cleanly: empty task arrays, limit greater than task count, and early rejection handling.

The Interview Problem

What is logged to the console when the following promisePool utility executes four tasks with variable delays under a concurrency limit of 2?

1async function promisePool(tasks, limit) {
2 const results = [];
3 const executing = new Set();
4 let peak = 0;
5
6 for (const [index, task] of tasks.entries()) {
7 const p = Promise.resolve().then(task).then((res) => {
8 results[index] = res;
9 executing.delete(p);
10 });
11 executing.add(p);
12 peak = Math.max(peak, executing.size);
13 if (executing.size >= limit) {
14 await Promise.race(executing);
15 }
16 }
17 await Promise.all(executing);
18 return { results, peak };
19}
20
21async function run() {
22 const logs = [];
23 const delayTask = (id, ms) => () =>
24 new Promise((resolve) =>
25 setTimeout(() => {
26 logs.push('done:' + id);
27 resolve(id * 10);
28 }, ms)
29 );
30
31 const tasks = [
32 delayTask(1, 30),
33 delayTask(2, 10),
34 delayTask(3, 20),
35 delayTask(4, 10)
36 ];
37
38 const { results, peak } = await promisePool(tasks, 2);
39 console.log(logs.join(' ') + ' | peak:' + peak + ' | res:' + results.join(','));
40}
41
42run();
Predict Console Output
Interactive Challenge

Select the option that matches what standard ECMAScript prints to the console:

done:2 done:1 done:3 done:4 | peak:2 | res:10,20,30,40

done:1 done:2 done:3 done:4 | peak:2 | res:10,20,30,40

done:2 done:1 done:3 done:4 | peak:4 | res:20,10,30,40

done:2 done:3 done:1 done:4 | peak:2 | res:10,20,30,40

V8 Engine Execution Trace
Step 1 of 6 (Line 6)

Task 1 (30ms duration) is enqueued and starts immediately. executing.size becomes 1.

Call Stack (Top = Active)
promisePool()
Task 1 Launch
Lexical Scope / Bindings
index:0
executing.size:1
limit:2
Microtask Queue (0)
Macrotask Queue (1)
setTimeout(Task1, 30ms)
Console Stream
> [empty]

Deep Technical Breakdown

Why Concurrency Pooling is Essential

Running thousands of asynchronous operations with Promise.all(tasks) attempts to initiate all operations simultaneously. In production web applications, this causes:

  1. Browser Network Connection Saturation: Browsers enforce a limit of 6 simultaneous TCP connections per domain HTTP/1.1; unbounded promises cause request queuing and timeouts.
  2. Database & API Rate Limits (HTTP 429): Downstream microservices crash under sudden traffic spikes.
  3. Node.js Memory Pressure: Thousands of allocated Promise callbacks and socket buffers overwhelm the V8 heap.

A Promise Concurrency Pool bounds simultaneous in-flight operations to a fixed limit $K$.

The Algorithm: Set + Promise.race

Tasks: [T1, T2, T3, T4]  (Limit = 2)

Time 0ms:   [ T1 (30ms), T2 (10ms) ]  ──► executing.size = 2 (await Promise.race)
Time 10ms:  T2 completes! Slot freed. Launch T3.
            [ T1 (20ms left), T3 (20ms) ]
Time 30ms:  T1 & T3 complete! Slots freed. Launch T4.
            [ T4 (10ms) ]
Time 40ms:  T4 completes. All tasks settled.

Result Ordering Guarantee

Even though tasks finish out of order (Task 2 before Task 1), results must preserve original task input order:

javascript
// DO NOT USE: results.push(res)  <-- Corrupts order!
// INSTEAD USE:
results[index] = res; // Guarantees index correspondence

Concurrency vs Rate Limiting

  • Concurrency: Limits the number of simultaneously in-flight requests (e.g., at most 5 at any instant).
  • Rate Limiting: Limits the frequency of requests over time (e.g., at most 10 requests per second, using Leaky Bucket or Token Bucket algorithms).
Common Traps & Mistakes

Using `results.push(res)` instead of `results[index] = res`, causing the results array to be ordered by completion time rather than input index.

Creating an array of promises upfront (`tasks.map(t => t())`), which immediately fires all tasks simultaneously before the concurrency limiter can take effect.

Failing to handle task rejection: an unhandled rejection inside `Promise.race` crashes the pool unless tasks are wrapped in `.catch()` or structured with try/catch.

Not awaiting the remaining active tasks after the main loop: `await Promise.all(executing)` is required to wait for the final batch of in-flight promises.

FAANG Follow-Up Probes
Probe #1

How would you modify `promisePool` to support `allSettled` semantics where individual task failures do not abort the entire pool?

Probe #2

How can you add dynamic priority support so high-priority tasks jump ahead of lower-priority tasks in the queue?

Probe #3

How would you integrate an `AbortSignal` to cancel all remaining pending tasks if the user navigates away?