Encodera

Thanks for reading!

Donate to the site — we work hard for you.

How to run async tasks in parallel with a limit

Short answer

Start N workers. Each worker takes the next task from a shared counter until the tasks run out:

async function mapWithLimit(items, limit, fn) {
  const results = new Array(items.length);
  let next = 0;
  const worker = async () => {
    while (next < items.length) {
      const index = next++;
      results[index] = await fn(items[index]);
    }
  };
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
  return results;
}
 
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let running = 0;
let maxRunning = 0;
 
const results = await mapWithLimit([1, 2, 3, 4, 5, 6, 7], 3, async (n) => {
  running++;
  maxRunning = Math.max(maxRunning, running);
  await delay(50);
  running--;
  return n * 10;
});
 
console.log(results.join(', '));
console.log('at most at once:', maxRunning);

Output:

10, 20, 30, 40, 50, 60, 70
at most at once: 3

With fetch

That is how you download a hundred pages without overloading the server:

const ids = Array.from({ length: 100 }, (_, i) => i + 1);
const products = await mapWithLimit(ids, 5, (id) => fetch(`/api/products/${id}`).then((r) => r.json()));

In batches

A simpler way is batches: run N tasks, wait for all of them, then the next N. It is a little slower, because each batch waits for its slowest task:

const delay = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
const items = [1, 2, 3, 4, 5];
const results = [];
 
for (let i = 0; i < items.length; i += 2) {
  const batch = items.slice(i, i + 2);
  results.push(...(await Promise.all(batch.map((n) => delay(20, n * n)))));
}
 
console.log(results.join(', '));

Output:

1, 4, 9, 16, 25

Common mistake

items.map(fn) starts all the tasks at once. Promise.all() only waits for them; it does not limit anything:

let running = 0;
let maxRunning = 0;
const task = async () => {
  running++;
  maxRunning = Math.max(maxRunning, running);
  await new Promise((resolve) => setTimeout(resolve, 10));
  running--;
};
 
await Promise.all(Array.from({ length: 50 }, task));
 
console.log(maxRunning);

Output:

50

Functions used in this recipe

  • Promise.all — Waits for all promises in parallel.
  • async function — A function that always returns a promise and lets you use await inside.

Thanks for reading!

Donate to the site — we work hard for you.