asynchronous programming
HTTP requests
concurrency
web development
API integration

Making multiple HTTP requests asynchronously

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Making several HTTP requests asynchronously lets your program wait for network responses in parallel instead of one by one. The core idea is simple: start the requests first, then await their results together, while still handling failures and rate limits deliberately.

Run Requests Concurrently with Promise.all

In modern JavaScript, the usual tool is Promise.all. It takes an array of promises and resolves when all of them succeed.

javascript
1const urls = [
2  "https://jsonplaceholder.typicode.com/posts/1",
3  "https://jsonplaceholder.typicode.com/posts/2",
4  "https://jsonplaceholder.typicode.com/posts/3"
5];
6
7const responses = await Promise.all(
8  urls.map((url) => fetch(url))
9);
10
11const data = await Promise.all(
12  responses.map((response) => response.json())
13);
14
15console.log(data);

This is concurrent, not sequential. All three requests start before the code waits for the results. That usually makes total runtime closer to the slowest request instead of the sum of all request times.

Know the Difference Between Sequential and Concurrent

This loop is sequential:

javascript
1for (const url of urls) {
2  const response = await fetch(url);
3  console.log(await response.json());
4}

Each request waits for the previous one to finish. That is sometimes correct, but not if the requests are independent and you want speed.

The concurrent version starts them together:

javascript
const tasks = urls.map((url) => fetch(url));
const responses = await Promise.all(tasks);

That change in structure is what makes the code asynchronous in a useful way.

Handle Partial Failure with Promise.allSettled

Promise.all fails fast: if one request rejects, the whole combined promise rejects. Sometimes that is what you want. In other cases, you want successful results even if one request fails.

Use Promise.allSettled for that:

javascript
1const results = await Promise.allSettled(
2  urls.map((url) => fetch(url))
3);
4
5for (const result of results) {
6  if (result.status === "fulfilled") {
7    console.log("Success:", result.value.status);
8  } else {
9    console.error("Failed:", result.reason);
10  }
11}

This is useful for dashboards, batch fetches, or aggregation jobs where one bad endpoint should not discard every other successful response.

Limit Concurrency When the Request Count Is Large

Asynchronous does not mean "fire ten thousand requests at once." Too much concurrency can hit API rate limits, exhaust sockets, or overwhelm the remote service.

A simple batching approach is often enough:

javascript
1async function fetchInBatches(urls, batchSize) {
2  const results = [];
3
4  for (let index = 0; index < urls.length; index += batchSize) {
5    const batch = urls.slice(index, index + batchSize);
6    const responses = await Promise.all(batch.map((url) => fetch(url)));
7    const data = await Promise.all(responses.map((response) => response.json()));
8    results.push(...data);
9  }
10
11  return results;
12}
13
14const posts = await fetchInBatches(urls, 2);
15console.log(posts.length);

This keeps concurrency controlled without giving up the performance benefits of overlapping I/O.

Common Pitfalls

The biggest mistake is writing await inside a loop and assuming the code is still concurrent. If each iteration waits immediately, the requests are sequential.

Another issue is ignoring error handling. Network calls fail in normal operation, so decide whether one failure should cancel the whole batch or only mark one item as failed.

Rate limits are also easy to underestimate. Even if your code works locally, production traffic can overwhelm an API if concurrency is unbounded.

Finally, do not forget response parsing. fetch() resolves for HTTP error statuses too, so check response.ok when the status code matters instead of assuming every resolved promise means success.

Summary

  • Start independent HTTP requests first, then await them together.
  • Use Promise.all when every request must succeed.
  • Use Promise.allSettled when partial success is acceptable.
  • Limit concurrency for large batches to avoid rate limits and resource pressure.
  • Remember that fetch() resolving does not automatically mean the HTTP status was successful.

Course illustration
Course illustration

All Rights Reserved.