Logo
Published on

How Concurrent is Promise.all in NodeJS

Introduction

Promise.all itself doesn't introduce concurrency—it merely waits for the promises to resolve. It's the nature of the underlying promises that determines how they are executed (whether concurrently or not).

nodejs-promise-all-concurrency
Table of Contents

How Promise.all Actually Works

Promise.all takes an iterable (usually an array) of promises and returns a single promise that resolves once all of the promises in that iterable have resolved. If any of the promises reject, it immediately rejects with the reason from the first promise that fails.

However, Promise.all does not inherently handle concurrency or parallelism. It simply accepts the promises that you pass to it. If these promises start executing concurrently (like in the case of I/O-bound tasks such as network requests), it's because the tasks themselves are asynchronous and start running as soon as they're created, not because of Promise.all.

Network Requests

When you make network requests or perform any asynchronous I/O operations, they naturally run concurrently. Promise.all waits for all of them to complete, but it doesn't schedule or control their execution:

const fetchData = async () => {
  const api1 = fetch('https://api.example.com/data1')
  const api2 = fetch('https://api.example.com/data2')
  const api3 = fetch('https://api.example.com/data3')

  try {
    const [data1, data2, data3] = await `Promise.all`([api1, api2, api3])
    console.log('Data fetched:', data1, data2, data3)
  } catch (error) {
    console.error('Error fetching data:', error)
  }
}

fetchData()

In the above example, as soon as the three fetch calls are made, they start fetching data concurrently (since network I/O is non-blocking). Promise.all simply waits for all these promises to resolve but doesn't control how they are executed.

Promise.all and Parallelism

JavaScript, being single-threaded, does not provide native multithreading for CPU-bound tasks. So, if you want parallelism for CPU-heavy operations, Promise.all won't help. Tasks like image processing, large computations, etc., will still run on the main thread, blocking other operations unless you offload them to Web Workers or child processes.

For I/O-bound operations (e.g., file reading, HTTP requests), JavaScript's event loop and runtime (Node.js or browsers) will run them concurrently if they are asynchronous, and this is what makes Promise.all appear "concurrent." But it's the async nature of the tasks, not Promise.all itself, doing the work.

Example: Coffee Shop Order Processing

In a coffee shop, we need to process orders concurrently. Baristas are responsible for processing orders. Each order takes some time to complete. Here, for breviety, we'll assume that each order takes of 5 to 10 seconds to complete.

Coffee Shop Order Processing

Orders: Each order represents a task or operation in Node.js.

Baristas: The number of baristas represents the concurrency level, simulating how many tasks Node.js can process simultaneously.

Order Queue: Orders are processed based on the number of available baristas (concurrency level).

You can add orders (tasks) to the queue using the "Add Order" button. The concurrency level (number of baristas) can be adjusted using the slider (1-5).

When you click on the "Process Orders" button, the simulation starts:

  • Orders are processed simultaneously up to the concurrency limit.
  • Each order takes 5-10 seconds to complete.
  • Active orders (being processed) have a green progress bar.
  • Orders in the queue show a "waiting" status.

As orders are completed, new orders from the queue start processing. The simulation continues until all orders are completed.

Performance Consideration: Too Many Promises

While Promise.all can handle multiple promises, running too many promises concurrently (e.g., making thousands of API calls at once) can exhaust system resources.

In such cases, limiting concurrency with tools like p-limit or manually batching promises is a better approach:

const pLimit = require('p-limit')
const limit = pLimit(2)

const tasks = [
  () => fetchDataFromAPI('https://api.example.com/data1'),
  () => fetchDataFromAPI('https://api.example.com/data2'),
  () => fetchDataFromAPI('https://api.example.com/data3'),
]

const limitedPromises = tasks.map((task) => limit(task))

;`Promise.all`(limitedPromises).then((results) => {
  console.log(results)
})

Promise.all itself does not handle concurrency; it simply waits for promises to resolve. The concurrency comes from the asynchronous nature of the operations you pass to it, like HTTP requests, file I/O, etc.

If those operations can be performed concurrently, they will, and Promise.all will wait for all of them to finish before proceeding.