Run asynchronous functions concurrently in Node.js

Start independent asynchronous work together, choose the right promise combinator, and limit concurrency when downstream capacity is finite.

Best practices
Illustration accompanying a Node.js development guide

Call independent asynchronous functions before awaiting their combined result. Promise.all() then waits for all of them to fulfill and returns results in input order. It coordinates promises; it does not create threads or start function objects for you.

Sequential versus concurrent calls

async function sequential(loadUser, loadOrders) {
  const user = await loadUser();
  const orders = await loadOrders();
  return { user, orders };
}

async function concurrent(loadUser, loadOrders) {
  const [user, orders] = await Promise.all([loadUser(), loadOrders()]);
  return { user, orders };
}

Use the second form only when loading orders does not depend on the user result. Function calls are evaluated in sequence, including their synchronous prefixes, but their asynchronous I/O can overlap. Total time depends on contention and service behavior; it is not guaranteed to equal the slowest individual request exactly.

For an array, await Promise.all(ids.map(loadById)) collects one operation per ID. Pass a wrapper such as ids.map((id) => loadById(id)) when the function should not receive map's extra index and array arguments.

Choose the result policy

Operation When it settles
Promise.all All fulfill, or one rejects
Promise.allSettled Every input settles; returns a status record for each
Promise.race The first input settles, whether fulfilled or rejected
Promise.any The first input fulfills; rejects with AggregateError if all reject

all and allSettled preserve input order in their result arrays. A rejection from all, or a winner from race, does not cancel the remaining operations. An AbortController helps only when the underlying API supports and receives its signal.

Catch errors at the level where you can make a decision: fail the entire request, retain partial results, or retry selected operations. Retry policies must account for operations that may already have completed, especially writes.

Bound the number in flight

A large Promise.all can overwhelm a connection pool, database, or remote rate limit. With an installed p-limit dependency:

import pLimit from 'p-limit';

async function loadBatch(ids, loadById) {
  const limit = pLimit(4);
  return Promise.all(ids.map((id) => limit(() => loadById(id))));
}

The function passed to the limiter delays the start of each operation until a slot is available. The example's four-operation limit is illustrative; choose a limit based on the downstream service and measurements. For enormous inputs, also avoid retaining every task and result in memory at once.

Concurrency is not CPU parallelism

Wrapping CPU-heavy JavaScript in an async function does not move it off the event-loop thread. Several such functions can still block one another. Use worker threads or separate processes when the measured bottleneck is CPU execution. Ordinary asynchronous network and filesystem operations often benefit from overlap without that additional machinery.

See Promise.all and p-limit.

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting