Node.js
Promises
JavaScript
Async Programming
Queue Management

node.js - Control a queue of Promises

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

When Node.js code creates too many promises at once, the event loop stays responsive but the surrounding systems may not. Databases, APIs, and disks can all become overloaded if every task starts immediately. The usual fix is to treat async work as a queue and limit how many promise-producing tasks run at the same time.

Understand the Real Problem

Promise.all does not schedule work gradually. It starts every task as soon as the task function is called. That is fine for a handful of operations, but it becomes a problem when you are sending hundreds of HTTP requests or processing thousands of files.

Bad pattern:

javascript
await Promise.all(items.map(item => processItem(item)));

If items is large, every call to processItem starts at once. A queue gives you backpressure by controlling concurrency explicitly.

Sequential Queue

The simplest queue is fully sequential. Only one task runs at a time, which is often enough for rate-limited APIs or order-sensitive work.

javascript
1async function runSequentially(taskFactories) {
2  const results = [];
3
4  for (const createTask of taskFactories) {
5    const result = await createTask();
6    results.push(result);
7  }
8
9  return results;
10}
11
12function makeTask(id, delayMs) {
13  return async () => {
14    await new Promise(resolve => setTimeout(resolve, delayMs));
15    console.log(`finished task ${id}`);
16    return id;
17  };
18}
19
20const tasks = [
21  makeTask(1, 300),
22  makeTask(2, 200),
23  makeTask(3, 100),
24];
25
26runSequentially(tasks).then(console.log);

This is easy to reason about, but it may be slower than necessary if the downstream system can handle some parallelism.

Concurrency-Limited Queue

Most real systems need something in between sequential execution and unlimited concurrency. A small queue runner can keep, for example, three tasks active at a time.

javascript
1async function runWithConcurrency(taskFactories, limit) {
2  const results = new Array(taskFactories.length);
3  let nextIndex = 0;
4
5  async function worker() {
6    while (true) {
7      const currentIndex = nextIndex;
8      nextIndex += 1;
9
10      if (currentIndex >= taskFactories.length) {
11        return;
12      }
13
14      results[currentIndex] = await taskFactories[currentIndex]();
15    }
16  }
17
18  const workers = Array.from(
19    { length: Math.min(limit, taskFactories.length) },
20    () => worker()
21  );
22
23  await Promise.all(workers);
24  return results;
25}
26
27function makeTask(id, delayMs) {
28  return async () => {
29    await new Promise(resolve => setTimeout(resolve, delayMs));
30    console.log(`completed ${id}`);
31    return `task-${id}`;
32  };
33}
34
35async function main() {
36  const tasks = [
37    makeTask(1, 500),
38    makeTask(2, 300),
39    makeTask(3, 200),
40    makeTask(4, 400),
41    makeTask(5, 100),
42  ];
43
44  const results = await runWithConcurrency(tasks, 2);
45  console.log(results);
46}
47
48main().catch(console.error);

This approach keeps output ordering stable because results are stored at the original task index, even though execution timing overlaps.

Handle Errors Deliberately

Queue control is not only about concurrency. It is also about deciding what should happen when one task fails. Some queues should stop on the first failure, while others should continue and collect errors for reporting.

javascript
1async function runWithCollection(taskFactories, limit) {
2  const outcomes = new Array(taskFactories.length);
3  let nextIndex = 0;
4
5  async function worker() {
6    while (true) {
7      const index = nextIndex++;
8      if (index >= taskFactories.length) {
9        return;
10      }
11
12      try {
13        outcomes[index] = { status: "fulfilled", value: await taskFactories[index]() };
14      } catch (error) {
15        outcomes[index] = { status: "rejected", reason: error.message };
16      }
17    }
18  }
19
20  await Promise.all(
21    Array.from({ length: Math.min(limit, taskFactories.length) }, () => worker())
22  );
23
24  return outcomes;
25}

This pattern is useful for batch jobs where you want a complete report rather than an immediate stop.

Use Libraries When the Rules Get Richer

If the queue needs retries, priority, pausing, timeouts, or rate limits per second, a library may be more maintainable than custom code. The important point is to understand the underlying model first: the queue should hold task factories, not already-running promises. Once a promise has started, the queue can no longer control it.

Common Pitfalls

  • Passing already-created promises into a queue and assuming concurrency will still be limited.
  • Using Promise.all on a large batch and overwhelming the downstream service.
  • Forgetting to preserve result order when tasks finish at different times.
  • Ignoring error strategy and then getting half-finished work with no report.
  • Setting the concurrency limit too high and recreating the same overload problem.

Summary

  • Queue control in Node.js is really about limiting how many promise-producing tasks start at once.
  • Use sequential execution when order matters most and throughput is secondary.
  • Use a concurrency-limited worker loop when some parallelism is safe.
  • Queue task factories, not active promises.
  • Decide early whether failures should stop the queue or be collected for later reporting.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.