awesome-everything RU
↑ Back to the climb

Frontend Architecture

Fetch waterfalls — diagnosis and the Promise.all cure

Crux Why sequential awaits make total latency the sum of all fetches, how to spot waterfalls in the Network panel, and three patterns that eliminate them.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at middle altitude — in the sky
◷ 10 min

A profile page needs user info (200ms), their posts (300ms), and their follower count (150ms). None of these depend on each other. So why does the page take 650ms instead of 300ms?

What a waterfall is

A fetch waterfall is a series of requests where each one starts only after the previous one completes. The classic React waterfall:

  1. Component A mounts, fires fetch('/api/user') in useEffect
  2. Response arrives (200ms) — component re-renders, renders child B
  3. Component B mounts, fires fetch('/api/posts') in its useEffect
  4. Response arrives (300ms) — renders child C
  5. Component C fires fetch('/api/followers') (150ms)

Total: 650ms. Yet none of these three fetches depend on each other’s data.

Waterfall vs parallel — same data, 2× difference
Sequential (waterfall)
user 200ms
posts 300ms
followers 150ms
Total: 650ms
Parallel (Promise.all)
user 200ms
posts 300ms
followers 150ms
Total: 300ms (capped by slowest)

Why waterfalls form

The root cause is data colocation meeting sequential rendering. React renders the component tree top-down. Each component fires its own effect after mounting. The child component cannot mount until the parent renders, which happens only after the parent’s data arrives. Even if child and parent data are independent, the rendering dependency creates a fetch dependency.

The pattern is especially deadly in deeply nested trees: a 5-level component tree with one fetch per level adds 5 sequential round-trips even if every fetch is logically independent.

Fix 1: Promise.all at the parent

The simplest cure: hoist all independent fetches to a common parent and fire them in parallel.

// Bad — waterfall
const user = await fetchUser(id);
const posts = await fetchPosts(id);
const followers = await fetchFollowers(id);

// Good — parallel
const [user, posts, followers] = await Promise.all([
  fetchUser(id),
  fetchPosts(id),
  fetchFollowers(id),
]);

Total time drops from sum to max. With 5 independent 200ms fetches: 1000ms → 200ms.

Fix 2: TanStack Query useQueries

When using a client library, useQueries runs multiple queries in parallel from one hook call:

const results = useQueries({
  queries: [
    { queryKey: ['user', id], queryFn: () => fetchUser(id) },
    { queryKey: ['posts', id], queryFn: () => fetchPosts(id) },
    { queryKey: ['followers', id], queryFn: () => fetchFollowers(id) },
  ],
});

All three fire simultaneously. Each result gets its own loading/error state. The array index matches the query order.

Fix 3: RSC sibling components

In React Server Components, sibling async components fetch in parallel by default because the server can resolve them concurrently:

// Both fetch simultaneously on the server
async function ProfilePage({ id }) {
  return (
    <div>
      <UserCard id={id} />   {/* fetches user */}
      <PostsList id={id} />  {/* fetches posts — in parallel with UserCard */}
    </div>
  );
}

The key: do not await one before rendering the other. Keep them as siblings, let React’s server rendering dispatch them concurrently.

Why this works

The fetch waterfall is structurally identical to a database N+1 query. A list of 10 items, each needing a second fetch, produces 11 sequential round-trips instead of 2. Same diagnosis, same cure: lift and batch. The names differ (N+1 vs waterfall) but the problem is the same: sequential when parallel was possible.

Waterfall cost in practice
Typical round-trip (fast network)
50–200 ms
5 sequential independent fetches
250–1000 ms
5 parallel fetches (Promise.all)
50–200 ms (max only)
Promise.all speedup at N=5
~5x
Waterfall depth in large SPAs
3–8 levels typical
Complete the analogy

Fill in: a fetch _______ happens when fetch B cannot start until fetch A has returned, making total time A + B instead of max(A, B).

Quiz

A profile page has three useEffect fetches — user, posts, followers. None depend on each other. What is the total fetch time?

Quiz

You convert three sequential server-side awaits to Promise.all. What does total fetch time become?

Recall before you leave
  1. 01
    Why do component-level useEffect fetches form waterfalls even when the data is independent?
  2. 02
    Name three patterns that eliminate a fetch waterfall.
  3. 03
    What is the N+1 / waterfall structural equivalence?
Recap

A fetch waterfall serialises independent requests through the render cascade: each component mounts only after its parent’s data arrives, so total latency equals the sum of every fetch in the chain. The fix is parallel dispatch — Promise.all at the parent for server code, useQueries for client hooks, or sibling RSC async components on the server. The speedup at N=5 parallel fetches vs sequential is approximately 5x. Spotting waterfalls is straightforward in Chrome DevTools: look for a staircase pattern in the Network panel where each request starts exactly when the previous one completes.

Connected lessons
appears again in178
Continue the climb ↑React Server Components and Suspense streaming
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.